#!/usr/bin/env python # -*- coding: utf-8 -*- # Original code by Planet-Work # Forked by Malin Cenusa for Lunarpages (malin.cenusa@lunarpages.com) # # To Do: # - Organize # - Translate # - Add more patterns # import os import re import fnmatch whitelist = [ '/lp-msh-scanner/', '/lp-msh-scanner/', '/._', 'cache/object/000000/', 'libraries/simplepie/simplepie.php', '/smarty/cache/', 'SimplePie/Misc.php', 'libraries/phpxmlrpc/xmlrpc.php', '/typography/googlefonts.php', '/var/cache/deliverycache_', 'GPAO/ajouter_clients.php', '/GPAO/modifier_clients2.php', 'libraries/openid/Auth/OpenID/Consumer.php', '/includes/utf/data/', 'libraries/openid/Auth/OpenID/Association.php', '/module_courriel/form_courriel.php', '/pro-settings.php', '/themes/MmoPress/header.php', '/themes/MmoPress/functions.php', 'tcpdf.php', '/wp-includes/upgrade.php', '/includes/et_lb_sample_layouts.php', '/sp_compatibility_test.php', '/tmp/cache/skel/html____', '/assets/styles/css-', '/help.inc.php', 'achat/fin_commande.php', '/accessibility.inc.php', '/optimizePressPlugin/lib/', '/wfBrowscapCache.php', 'kick-it-2x/footer.php', '/blueidea-10/footer.php', '/three_tennis_balls_scoreboard_spj005/footer.php', '/page-google-maps/view/widget_js.php', '/gardens-amidst-jungle/footer.php', '/With_Rainbows/footer.php', '/naturetravel/footer.php', '/pages/1250793045.php', '/tinymce/preview.php', 'polldata/session.php', '/tmplvars.inc.php', '/Command/Factory/FactoryInterface.php', '/Service/Exception/DescriptionBuilderException.php', 'iconic-navigation/iconavs_icons.php', '/classes/TinyPspellShell.class.php', 'aklazy/aklazy/main.php', '/inc/RecupDocsTheme.php', '/function.eval.php', '/trans_box.php', 'newstoday/footer.php', '/md_mix.inc.php', '/import.inc.php', '/cache/twig/', '/transliteration/data/', '/Transliterator/data/x', '/patterns/', '/ecrire/lang/public_', 'phocagallery/render/renderinfo.php', 'Faker/Provider', '/ecrire/lang/ecrire_', '/ecrire/lang/spip_', '/symfony/vendors.php', 'stat/images/os.php', 'js_composer/config/templates.php', 'leaflet-maps-marker/leaflet-exportcsv.php', '/auth/iso639-2.php', '/includes/lang/.*\.inc\.php', '.yml.php', '.js.php', 'cache/siteCache.idx.php', '/wpsr-services-selector.php', '/mpdf56/examples', '/mpdf50/examples', 'wp-admin-bar-removal/wp-admin-bar-removal.php', 'includes/admin/dummy.php', 'tcpdf/fonts/', '/shortcodes/googlemaps.php', '/infocus/activation.php', 'admin/core/core-help-text.php', '/theme-check/checks/badthings.php', '_compatibility_test/sdk_compatibility_test.php', 'compatibility_test/sp_compatibility_test.php', '/panel/shortcodes/ui.php', 'tcpdf/fonts/pdfasymbol.php', 'libraries/facebook-php-sdk/src/base_facebook.php', 'Auth/NTLMAuthenticatorTest.php', 'vendor/vendors.php', '/bin/vendors.php', 'ap_ProdProjectContainer.php', 'lib/htmLawed.php', '/editors/xinha.php', '/akismet/views/notice.php', 'System/Model/Base/RouteGateway.php', 'System/Model/Base/AdminrouteGateway.php ', 'Comment/Model/Base/PostGateway.php', '/Sluggable/Util/data/x', '/ttfontdata/', 'GDEFdata.php', 'includes/facebook-php-sdk/base_facebook.php', 'wp-content/plugins/wysija-newsletters/helpers/render_engine.php', 'wp-content/plugins/wysija-newsletters/views/back/campaigns.php', 'wp-content/plugins/wysija-newsletters/helpers/render_engine.php', 'wp-content/plugins/broken-link-checker/idn/uctc.php', 'wp-includes/class-IXR.php', 'wp-includes/SimplePie/Sanitize.php', 'wp-admin/includes/ajax-actions.php', 'wp-content/plugins/codestyling-localization/codestyling-localization.php', '/nusoap.php', 'shortcodes/vc_raw_html.php', '/class-pclzip.php', '/pclzip/pclzip.lib.php', '/pclzip/pclzip.php', '/inc_php/framework/base_admin.class.php', 'wp-includes/class-wp-atom-server.php', 'wp-includes/class-simplepie.php', 'wp-includes/class-wp-customize-widgets.php', 'wp-admin/includes/file.php', 'wp-admin/js/revisions-js.php', '/wp-app.php', '/CallbackColumn.php', 'sitepress-multilingual-cms/res/languages.csv.php', 'content/plugins/better-wp-security/core/class-itsec-core.php', 'content/plugins/w3-total-cache/lib/W3/Plugin/Minify.php', 'content/plugins/w3-total-cache/lib/SNS/sdk.class.php', 'content/plugins/codestyling-localization/codestyling-localization.php', '_tcpdf/tcpdf.php', 'tcpdf/examples/example_', 'src/ext/htmlsql.class.php', 'plugins/wplite/wplite.php', 'plugins/nospamnx/ws1.php', 'bepro-listings/bepro_listings.php', '/wpposticon.php', '/better-wp-security/core/class-itsec-core.php', '/app/cache/dev/', '/app/cache/prod/', 'administrator/components/com_remository/admin.remository.html.php', 'administrator/components/com_remository/admin.remository.html.php', 'ultimate-coming-soon-page/framework/framework.php', 'wp-content/plugins/shortcodes-ultimate/inc/core/shortcodes.php', 'wp-content/plugins/shortcodes-ultimate/inc/vendor/sunrise.php', 'wp-content/plugins/ultimate-coming-soon-page/framework/framework.php', '/XmlRpcClientRemote/XmlRpc.php', 'sitepress-multilingual-cms/inc/installer/includes/installer.class.php', '/Amf/Server.php', 'src/facebook.php', '/spellchecker.php', 'util/php/ajax/filters.php', 'lib/class/SEO_URL.class.php', 'ebservice/dispatcher.php', 'include/js/jsval.php', 'include/js/lytebox.php', 'include/class.TCPDF.php', 'SimplePie/Sanitize.php', 'plugins/gravityforms/common.php', 'plugins/gravityforms/form_detail.php', 'gravityforms/includes/addon/class-gf-results.php', 'w3-total-cache/inc/functions/multisite.php', '/ezpublish/cache/', 'administrator/components/com_securitycheckpro/scans/', 'custom-fields/typography/googlefonts-array.php', 'wp-content/uploads/sucuri/sucuri-sitecheck.php', 'wp-content/plugins/akeebabackupcore/app/restore.php', ] debug = True line_early = 15 scoring = { 'WHITELISTED': (-10, u'fichier en whitelist manuelle'), 'WHITELISTED_LINE': (-10, u'ligne en whitelist manuelle'), 'PHP_COMMENTS': (-10, u'Fichier commence proprement avec une description'), 'CLASS_FUNCTION': (-10, u'Fonction ou classe définie au tout début'), 'BASE64_STRING': (50, u'Motif base64 trouvé'), 'CRYPT_PHP': (50, u'Script CryptPHP qui inclue social.png'), 'PHP_SHELL': (50, u'Script Shell'), 'PHP_OBFUSC_SHELL': (50, u'Script Shell caché'), 'ACCESS_DENIED': (-30, u'Execution bloquée en tout début de fichier'), 'JAVASCRIPT_HACK': (50, u'Hack javascript'), 'HAS_EVAL': (2, u'Contient eval()'), 'HAS_EVAL_EARLY': (10, u'Contient eval() en début de fichier'), 'HAS_CALL_FUNC_EARLY': (3, u'Contient call_user_func() en début de fichier'), 'HAS_BASE64DECODE': (2, u'Contient base64_decode() ou str_rot13()'), 'HAS_BASE64DECODE_EARLY': (10, u'Contient base64_decode() ou str_rot13() en début de fichier'), 'HAS_MAIL': (1, u'Contient mail()'), 'HAS_MAIL_EARLY': (2, u'Contient mail() en début de fichier'), 'LONG_LINE': (5, u'Contient une ligne de plus de 1000 caractères'), 'LONG_LINE_EARLY': (8, u'Contient une ligne de plus de 1000 caractères début de fichier'), 'VERY_LONG_LINE': (5, u'Contient une ligne de plus de 3000 caractères'), 'VERY_LONG_LINE_EARLY': (9, u'Contient une ligne de plus de 3000 caractères en début de fichier'), 'MD5_VAR': (10, u'Contient une variable encodée en MD5'), 'INCLUDE_REQUIRE': (-2, u'Contient include() ou require() sans http'), 'COOKIE_FORM1': (20, u'Contient form1=@$_COOKIE'), 'MAIL_X_HEADER': (5, u'Contient mail.add_x_header'), 'SET_TIME_0': (5, u'Contient set_time_limit(0)'), 'SET_ERRORREPORTING_0': (2, u'Contient error_reporting(0)'), 'SET_TIMELIMIT_0': (2, u'Contient memory_limit(0)'), 'SET_IGNOREUSERABORT_0': (2, u'Contient ignore_user_abort()'), 'UPLOAD_FILE': (2, u'Contient move_uploaded_file()'), 'FEW_LINES': (0, u'Contient peu de lignes'), 'EMPTY_FILE': (-100, u'Fichier vide'), 'MANY_LINES': (-2, u'Contient beaucoup de lignes'), 'MANY_LINES2': (-5, u'Gros fichier avec de lignes'), 'MANY_LINES3': (-10, u'Très gros fichier avec de lignes'), 'BAD_NEWLINES': (-5, u'Ficher sur 1 ligne sans saut de ligne'), 'NO_PHP_START': (-5, u'Ne commence pas par 50: score.append(('CONCAT_STRING', '%i concat' % (l.count('","') + l.count("'.'")))) if l.count('$GLOBALS[') > 20: score.append( ('MANY_GLOBALS', '%i globals' % (l.count('","') + l.count("'.'")))) l = l.replace('","', '').replace("'.'", '') if ('die("Access Denied");' in l or '' in l or '' in l or l.find('' in l or '= 0: score.append(('BIN_HOST', '')) if ('if( !isset($gCms) ) exit;' in l or "if( !defined( '_VALID_MOS' )" in l or "if (!defined('IN_PHPBB')" in l or "defined('_JEXEC') " in l or "or more information: see languages.txt in the la" in l) \ and line_num == 2: score.append(('ACCESS_DENIED', '')) # if 'Restricted access' in l: # print line_num if line_num in [2, 3] and ('Direct Access to this location is not allo' in l or 'Restricted access' in l or 'defined(\'_JEXEC\') or die' in l): score.append(('ACCESS_DENIED', '')) if line_num == 1 and not l.strip().find('' . $contents);" in l and not "_eval(" in l: if line_num < line_early: score.append(('HAS_EVAL_EARLY', 'line %i' % line_num)) else: score.append(('HAS_EVAL', 'line %i' % line_num)) if l.find('mail(') == 0 or ' mail(' in l: if line_num < line_early: score.append(('HAS_MAIL_EARLY', 'line %i' % line_num)) else: score.append(('HAS_MAIL', 'line %i' % line_num)) if line_num < line_early and 'call_user_func' in l: score.append(('HAS_CALL_FUNC_EARLY', 'line %i' % line_num)) if 'agent' in l.lower() and 'google' in l.lower(): score.append(('UA_GOOGLE', '')) if 'base64_decode(' in l or 'base64_decode (' in l \ or 'str_rot13(' in l or 'str_rot13 (' in l: if 'CmVycm9yX3JlcG9ydGluZygwKTsKJHFhe' in l or 'FZY1EuQIEkXvMlZ3yBBTrCVmLkH' in l or '" in l or 'b+=String.fromCharCode(a.charCodeAt(i)^2' in l or "eval(eval('String.fromCharCode(" in l: score.append(('JAVASCRIPT_HACK', '')) elif "'FilesMan';" in l or '"FilesMan";' in l or 'Web Shell by Guest' in l or 'File uppato senza problemi' in l or 'echo"gagal"' in l \ or "wpplugin_action = 'WPcheckInstall'" in l or "'bas'.'e6'.'4_d'.'ecode'" in l \ or 'shell_exec("sh inst")' in l or 'index.php replaced successufuly!' in l \ or 'NShell t35' in l \ or 'CorporateSignonTelecode' in l \ or 'UDP Shell!' in l \ or 'eval("?>".gzuncompress(base64_decode(' in l \ or 'Mr.HarchaLi' in l \ or ('eval(gzinflate(base64_decode(' in l and line_num < 5)\ or 'dropsforums.ru' in l \ or 'DamaneDz' in l \ or "$s='str_r'.'o'.'t13';" in l \ or '){type1_send();exit();}elseif(isset' in l \ or '\\x65\\x76\\x61\\x6c\\x20\\x28\\x20\\x67\\x7a\\x69\\x6e\\x66\\x6c\\x61\\x74' in l \ or 'CH (UBS Spam) ' in l \ or "$words['cantbeshown']" in l \ or '"netstat -an' in l \ or '($action==""||$password==""||$filename==""||$body=="")' in l \ or "strrev('edoced_46esab')" in l \ or (l.find("return base64_decode($") == 0 and 'for($i=0; $i < strlen($' in previous_line) \ or 'function multiRequest($data, $options = array(), $oneoptions = array())' in l \ or (l.find('GIF89') == 0 and line_num == 1) \ or (line_num == 1 and "@$_COOKIE[" in l and "();}?>" in l) \ or (line_num == 1 and '@move_uploaded_file' in l) \ or ("move_uploaded_file/*;*/" in l) \ or 'Database Emails Extractor' in l \ or ("

!PhpSend!

" in l) \ or 'Done ==> $userfile_name' in l \ or ('$files=fopen(\'../../../\'.$filepaths.' in l and ',"w+");' in l) \ or "chmod ($_REQUEST['p1'], $_REQUEST['p2']);" in l \ or "\\x62\\x61\\x73\\x65\\x36\\x34\\x5F\\x64\\x65\\x63\\x6F\\x64\\x65" in l \ or "\\x73\\x74\\x72\\x5f\\x72\\x6f\\x74\\x31\\x33" in l \ or "\\x67\\x7a\\x75\\x6e\\x63\\x6f\\x6d\\x70\\x72\\x65\\x73\\x73" in l \ or (line_num == 2 and "$ref = $_SERVER['HTTP_USER_AGENT'];" in l) \ or (line_num < 4 and "passthru($_POST[" in l) \ or (line_num == 1 and '$stg="ba"."se"."64_d"."ecode";eval($stg(' in l) \ or '(edoced_46esab(etalfnizg(lave' in l \ or "file_put_contents('1.txt', print_r" in l: score.append(('PHP_SHELL', '')) if 'move_uploaded_file(' in l: score.append(('UPLOAD_FILE', '')) if ('" in l ) \ or (line_num == 1 and 'eval(' in l and '$_REQUEST[' in l and ' = fopen' in l and '; exit(); } ?>' in l) \ or (line_num == 1 and 'if(!isset($GLOBALS[' in l) \ or (line_num == 2 and 'if(!empty($_POST[' in l and '){eval($_POST' in l) \ or (line_num == 2 and 'if(isset($_POST[' in l and 'eval($_POST[' in l) \ or '%x5c%x7825-bubE' in l: cleanup_available = True score.append(('PHP_SHELL', '')) if ('"' in l or "'" in l) and not '$UTF8_TO_ASCII' in previous_line: if len(l) > 3000 and not has_very_long_line: has_long_line = has_very_long_line = True if line_num < line_early: score.append( ('VERY_LONG_LINE_EARLY', 'line %i' % line_num)) else: score.append(('VERY_LONG_LINE', 'line %i' % line_num)) elif len(l) > 1000 and not has_very_long_line and not has_long_line: has_long_line = True if line_num < line_early: score.append(('LONG_LINE_EARLY', 'line %i' % line_num)) else: score.append(('LONG_LINE', 'line %i' % line_num)) if "$cidinfo['uni2cid'] = array(" in l or 'php return unserialize(' in l \ or ('") in l: score.append(('EITEST', '')) previous_line = l if line_num < 20: score.append(('FEW_LINES', '%i lines' % line_num)) elif line_num < 100: score.append(('MANY_LINES', '%i lines' % line_num)) elif line_num < 1000: score.append(('MANY_LINES2', '%i lines' % line_num)) else: score.append(('MANY_LINES3', '%i lines' % line_num)) # Shell super bien caché, toutes les lignes ont la même longueur if len(first_lines) > 12 and line_num < 30 and first_lines[0] == ' 40 and first_lines[3][0] == ' ': score.append(('PHP_OBFUSC_SHELL', '')) if line_num == 0 or (line_num == 1 and ( len(first_lines[0]) < 10 or 'Silence is golden.' in first_lines[0])) \ or (line_num == 2 and len(first_lines[0]) < 10 and 'Silence is golden.' in first_lines[1]): score.append(('EMPTY_FILE', '')) if line_num == 1 and (' 7 and (first_lines[0] == ' 3 and ('@author : ' in first_lines[1] or '@author : ' in first_lines[0] or '@version' in first_lines[2] or 'Legacy Mode compatibility' in first_lines[2]): score.append(['PHP_COMMENTS', w]) total_score = 0 score_details = [] score_done = [] for sco, detail in score: if sco in score_done: continue score_done.append(sco) total_score += scoring[sco][0] score_details.append({'rule': sco, 'details': detail.encode('utf-8'), 'score': scoring[sco][0], 'description': scoring[sco][1].encode('utf-8')}) if filename[0] != '/': filename = os.getcwd() + '/' + filename if clean_PCT4 or clean_evalbase64: cleanup_available = True if cleanup_available and line_num == 1: cleanup_available = False return {'filename': filename, 'score': total_score, 'mtime': os.stat(filename).st_mtime, 'ctime': os.stat(filename).st_ctime, 'details': score_details, 'cleanup': cleanup_available} # print total_score, filename, '::'.join(score_details).encode('utf-8') # from subprocess import Popen # if clean_PCT4: # print "PCT4", filename, "CLEANED" # Popen(['perl', '-pi', '-e','s/<\?php.*$sF=.PCT4B.*}\?>//g',filename]) # elif clean_evalbase64: # print "EVAL+BASE64", filename, "CLEANED" # Popen(['perl', '-pi', # '-e','s/\?php\s*eval\(base64_decode\("[a-zA-Z0-9\/=]*"\)\);/?php/g',filename]) if __name__ == '__main__': SERIALIZER = 'json' import argparse try: import yaml SERIALIZER = 'yaml' except ImportError: import json parser = argparse.ArgumentParser( description='Check directory or file for PHP malwares.') parser.add_argument('directory_file', help='directory or file to check', type=str) parser.add_argument('--post', dest='post', type=str, default=None, help='POST the result to an URL "%%TOKEN%%" will be \ replaced by a uuid random token and "%%HOSTNAME%%" by \ hostname (default: no post)') parser.add_argument('--minscore', dest='minscore', type=int, default=-5, help='Minimum score (default: -5)') parser.add_argument('--maxresults', dest='maxresults', type=int, default=500, help='Maximum number or results (default: 500)') args = parser.parse_args() basedir = args.directory_file RESULT_POST = args.post MIN_SCORE = args.minscore MAX_RESULTS = args.maxresults # print(args) # import socket # RESULT_POST = 'https://xxx.com/%s/%s' % (socket.gethostname(),token) if RESULT_POST: import socket import uuid RESULT_POST = RESULT_POST.replace('%HOSTNAME%', socket.gethostname()) RESULT_POST = RESULT_POST.replace('%TOKEN%', '%s' % uuid.uuid1()) results = [] if os.path.isdir(basedir): for root, dirnames, filenames in os.walk(basedir): for filename in filenames: if fnmatch.fnmatch(filename, '*.php') or \ fnmatch.fnmatch(filename, '*.js'): hacked = is_hacked(os.path.join(root, filename)) if hacked is not False and hacked['score'] >= MIN_SCORE: results.append(hacked) else: filename = basedir root = os.getcwd() if fnmatch.fnmatch(filename, '*.php') or \ fnmatch.fnmatch(filename, '*.js'): hacked = is_hacked(os.path.join(root, filename)) if hacked is not False and hacked['score'] >= MIN_SCORE: results.append(hacked) results.sort(key=lambda x: x['score'], reverse=True) if SERIALIZER == 'yaml': print(yaml.dump(results[0:MAX_RESULTS])) else: print(json.dumps(results[0:MAX_RESULTS], indent=4)) if RESULT_POST: import requests import urllib headers = { "Content-type": "application/x-yaml; charset=utf-8", "Accept": "text/plain" } if basedir[0] == '/': path = urllib.quote_plus(basedir[1::]) else: path = urllib.quote_plus(basedir) r = requests.post(RESULT_POST + '/' + path, data=yaml.dump(results[0:MAX_RESULTS]), headers=headers) print(r) print("=" * 100) print(r.text) print("URL : ", RESULT_POST)