精確查找PHP WEBSHELL木馬的方法(1)
更新時間:2011年04月12日 21:56:44 作者:
今天,我想了下,現(xiàn)在把查找PHP WEBSHELL木馬思路發(fā)出來,需要的朋友可以參考下。
先來看下反引號可以成功執(zhí)行命名的代碼片段。代碼如下:
`ls -al`;
`ls -al`;
echo "sss"; `ls -al`;
$sql = "SELECT `username` FROM `table` WHERE 1";
$sql = 'SELECT `username` FROM `table` WHERE 1'
/*
無非是 前面有空白字符,或者在一行代碼的結(jié)束之后,后面接著寫,下面兩行為意外情況,也就是SQL命令里的反引號,要排除的就是它。
*/
正則表達式該如何寫?
分析:
對于可移植性的部分共同點是什么?與其他正常的包含反引號的部分,區(qū)別是什么?
他們前面可以有空格,tab鍵等空白字符。也可以有程序代碼,前提是如果有引號(單雙)必須是閉合的。才是危險有隱患的。遂CFC4N給出的正則如下:【(?:(?:^(?:\s+)?)|(?:(?P<quote>["'])[^(?P=quote)]+?(?P=quote)[^`]*?))`(?P<shell>[^`]+)`】。
解釋一下:
【(?:(?:^(?:\s+)?)|(?:(?P<quote>["'])[^(?P=quote)]+?(?P=quote)[^`]*?))】
匹配開始位置或者開始位置之后有空白字符或者前面有代碼,且代碼有閉合的單雙引號。(這段PYTHON的正則中用了捕獲命名以及反向引用)
【`(?P<shell>[^`]+)`】這個就比較簡單了,匹配反引號中間的字符串。

某檢測PHP webshell的python腳本考慮欠佳。
再看看下一個列表的第一個元素?!?system|shell_exec|exec|popen)】,這個正則的意思是只要字符串里包含“system”、“shell_exec”、“exec”、“popen”這四組字符串即判定為危險字符。很明顯,這個方法太不嚴謹。如果程序員寫的代碼中,包含了這四組字符,即可被判定為危險函數(shù)。很不準確,誤報率極高。見下圖

某檢測PHP webshell的python腳本考慮欠佳。
到底什么樣的代碼是可疑的代碼?關(guān)鍵詞是什么?
可疑的代碼肯定是由可以執(zhí)行危險操作的函數(shù)構(gòu)成,可以執(zhí)行危險操作的PHP函數(shù)最重要的就是“eval”函數(shù)了,對于加密的PHP代碼(僅變形字符串,非zend等方式加密),肯定要用到“eval”函數(shù),所以,對于不管是用哪種加密方法的代碼,肯定要用到“eval”函數(shù)。其次就是可以執(zhí)行系統(tǒng)命令的函數(shù)了,比如上面某牛的代碼中提到的四個“system”、“shell_exec”、“exec”、“popen”。當然還有其他的,比如passthru等。PHP還支持“·”字符(ESC鍵下面那個)直接執(zhí)行系統(tǒng)命令。我們可以把正則寫成這樣【\b(?P<function>eval|proc_open|popen|shell_exec|exec|passthru|system)\b\s*\(】。

檢測PHP webshell的python腳本相對較為嚴謹?shù)钠ヅ?
解釋一下:
大家都知道【\b\b】用來匹配單詞兩邊的位置的。要保證【\b\b】中間的是單詞,即使函數(shù)名前面加特殊字符,也一樣通過匹配,比如加@來屏蔽錯誤。后面的【\s*】用來匹配空白字符的,包括空格,tab鍵,次數(shù)為0到無數(shù)次。前面的【(?P)】是捕獲命名組。用來當作python代碼直接引用匹配結(jié)果的key。
還有的網(wǎng)友提到了,如果我把代碼放到圖片拓展名的文件里呢?那你只檢測.php,.inc的文件,還是找不到我的呀。嗯,是的,如果惡意代碼在gif、jpg、png、aaa等亂七八糟的拓展名文件里,是不能被apache、IIS等web Services解析的,必須通過include/require(_once)來引入。那么,我們只要匹配include/require(_once)后面的文件名是不是常規(guī)的“.php”、“.inc”文件。如果不是,則為可疑文件。正則如下【(?P<function>\b(?:include|require)(?:_once)?\b)\s*\(?\s*["'](?P<filename>.*?(?<!\.(?:php|inc)))["']】。

檢測PHP WEBSHELL的python腳本較為嚴謹做法
解釋一下:
先看【(?P<function>\b(?:include|require)(?:_once)?\b)】,【(?P<name>)】為正則表達式的“命名捕獲”,PHP中有同樣的用法。也就是說,在這括號內(nèi)的捕獲的數(shù)據(jù),會分配到結(jié)果數(shù)組的key為“name”的value中。再看里面的【\b(?:include|require)(?:_once)?\b】,【\b\b】不解釋了,為單詞邊界位置。里面的【(?:include|require)】匹配字符串“include”、“require”兩個單詞,其中前面的【(?:)】未不分配組,用于提高效率,可以去掉【?:】變成【(include|require)】。在后面一個【(?:_once)】也是做不分配組的操作,便于提高正則表達式效率。同樣,后面的量詞是“?”代表這個組可有可無。就滿足了“include”、“include_once”、“require”、“require_once”四種情況。有的朋友可能這樣寫【(include|include_once|require|require_once)】也能實現(xiàn)目的。但是,為了更搞的效率,我們對這個正則做優(yōu)化,針對部分字符串做分支更改,改成上面那個【\b(?:include|require)(?:_once)?\b】。
再看下面的【\s*\(?\s*["'](?P<filename>.+?(?<!\.(?:php|inc)))["']】中,【\s*】匹配空白字符,包括空格,tab鍵等。后面的【\(?】,匹配字符“(”,后面的量詞“?”表示這半個小酷括號可有可無。防止“incude “123.php””這種沒有括號的情況。再后面【["']】匹配雙引號,單引號的。最后的也是。再看看這個【(?P<filename>.+?(?<!\.(?:php|inc)))】,其中【(?P<filename>)】上面介紹了,為命名捕獲,把結(jié)果放到match.group(“filename”)里。【.*?】為任意字符,后面的量詞是“忽略優(yōu)先量詞”,也就是平常說的“非貪婪”。這里最少匹配零個,(防止.aa、.htaccess這種沒有文件名,只有文件拓展名的文件被引入)。后面的【(?<!\.(?:php|inc))】,這里用到了反向零寬斷言(環(huán)視)的非操作(只匹配位置,不匹配字符串,跟【^$\b】等一樣)。這個表達式是針對這個位置的后面字符起作用的,也就是說后面的【["']】的前面不能是“.php”、“.inc”,這里也就是取了文件名的最后的拓展名。(正則里,可以用【^】對字符取非,但是不能對“字符串組”取非,這里用了零寬斷言來實現(xiàn)。)
綜上所述,最后,鄙人給出的python代碼如下:
#!/usr/bin/python
#-*- encoding:UTF-8 -*-
###
## @package
##
## @author CFC4N <cfc4nphp@gmail.com>
## @copyright copyright (c) Www.cnxct.Com
## @Version $Id: check_php_shell.py 37 2010-07-22 09:56:28Z cfc4n $
###
import os
import sys
import re
import time
def listdir(dirs,liston='0'):
flog = open(os.getcwd()+"/check_php_shell.log","a+")
if not os.path.isdir(dirs):
print "directory %s is not exist"% (dirs)
return
lists = os.listdir(dirs)
for list in lists:
filepath = os.path.join(dirs,list)
if os.path.isdir(filepath):
if liston == '1':
listdir(filepath,'1')
elif os.path.isfile(filepath):
filename = os.path.basename(filepath)
if re.search(r"\.(?:php|inc|html?)$", filename, re.IGNORECASE):
i = 0
iname = 0
f = open(filepath)
while f:
file_contents = f.readline()
if not file_contents:
break
i += 1
match = re.search(r'''(?P<function>\b(?:include|require)(?:_once)?\b)\s*\(?\s*["'](?P<filename>.*?(?<!\.(?:php|inc)))["']''', file_contents, re.IGNORECASE| re.MULTILINE)
if match:
function = match.group("function")
filename = match.group("filename")
if iname == 0:
info = '\n[%s] :\n'% (filepath)
else:
info = ''
info += '\t|-- [%s] - [%s] line [%d] \n'% (function,filename,i)
flog.write(info)
print info
iname += 1
match = re.search(r'\b(?P<function>eval|proc_open|popen|shell_exec|exec|passthru|system)\b\s*\(', file_contents, re.IGNORECASE| re.MULTILINE)
if match:
function = match.group("function")
if iname == 0:
info = '\n[%s] :\n'% (filepath)
else:
info = ''
info += '\t|-- [%s] line [%d] \n'% (function,i)
flog.write(info)
print info
iname += 1
f.close()
flog.close()
if '__main__' == __name__:
argvnum = len(sys.argv)
liston = '0'
if argvnum == 1:
action = os.path.basename(sys.argv[0])
print "Command is like:\n %s D:\wwwroot\ \n %s D:\wwwroot\ 1 -- recurse subfolders"% (action,action)
quit()
elif argvnum == 2:
path = os.path.realpath(sys.argv[1])
listdir(path,liston)
else:
liston = sys.argv[2]
path = os.path.realpath(sys.argv[1])
listdir(path,liston)
flog = open(os.getcwd()+"/check_php_shell.log","a+")
ISOTIMEFORMAT='%Y-%m-%d %X'
now_time = time.strftime(ISOTIMEFORMAT,time.localtime())
flog.write("\n----------------------%s checked ---------------------\n"% (now_time))
flog.close()
## 最新代碼在文章結(jié)尾的鏈接里給出了。2010/07/31 更新。
僅供參考,歡迎斧正。
下面截圖為掃描Discuz7.2的效果圖,當然,也有誤報。相對網(wǎng)上流傳的python腳本,誤報更少,更精確了。
檢測PHP WEBSHELL的python腳本的檢測結(jié)果
問:這個方法完美了嗎?可以查找目前已知的所有危險函數(shù)文件了嗎?
答:不能,如果include等引入的文件沒有拓展名,這里就匹配不到了。
問:如何解決?
答:留給你解決,聰明的你,肯定可以搞定。
PS:“`”反引號 執(zhí)行命令的還沒寫,暫時沒好的辦法。容易跟SQL語句中的反引號混淆。不太好匹配。如果光匹配反引號就提示的話,那誤報太大了。待定吧。(術(shù)業(yè)有專攻,請勿因為一處不好的代碼,否定一個人的能力。你懂的。再次重申,此文只針對代碼,不針對人。其次,鄙人給出的python代碼隨便復(fù)制,隨便傳播,愛留版權(quán)就留版權(quán),不愛留就刪了相關(guān)字符,也就是您愛干嗎干嗎。)
我先休息一會,明天再說。(前半句為三國殺曹仁的臺詞,哈。)
復(fù)制代碼 代碼如下:
`ls -al`;
`ls -al`;
echo "sss"; `ls -al`;
$sql = "SELECT `username` FROM `table` WHERE 1";
$sql = 'SELECT `username` FROM `table` WHERE 1'
/*
無非是 前面有空白字符,或者在一行代碼的結(jié)束之后,后面接著寫,下面兩行為意外情況,也就是SQL命令里的反引號,要排除的就是它。
*/
正則表達式該如何寫?
分析:
對于可移植性的部分共同點是什么?與其他正常的包含反引號的部分,區(qū)別是什么?
他們前面可以有空格,tab鍵等空白字符。也可以有程序代碼,前提是如果有引號(單雙)必須是閉合的。才是危險有隱患的。遂CFC4N給出的正則如下:【(?:(?:^(?:\s+)?)|(?:(?P<quote>["'])[^(?P=quote)]+?(?P=quote)[^`]*?))`(?P<shell>[^`]+)`】。
解釋一下:
【(?:(?:^(?:\s+)?)|(?:(?P<quote>["'])[^(?P=quote)]+?(?P=quote)[^`]*?))】
匹配開始位置或者開始位置之后有空白字符或者前面有代碼,且代碼有閉合的單雙引號。(這段PYTHON的正則中用了捕獲命名以及反向引用)
【`(?P<shell>[^`]+)`】這個就比較簡單了,匹配反引號中間的字符串。

某檢測PHP webshell的python腳本考慮欠佳。
再看看下一個列表的第一個元素?!?system|shell_exec|exec|popen)】,這個正則的意思是只要字符串里包含“system”、“shell_exec”、“exec”、“popen”這四組字符串即判定為危險字符。很明顯,這個方法太不嚴謹。如果程序員寫的代碼中,包含了這四組字符,即可被判定為危險函數(shù)。很不準確,誤報率極高。見下圖

某檢測PHP webshell的python腳本考慮欠佳。
到底什么樣的代碼是可疑的代碼?關(guān)鍵詞是什么?
可疑的代碼肯定是由可以執(zhí)行危險操作的函數(shù)構(gòu)成,可以執(zhí)行危險操作的PHP函數(shù)最重要的就是“eval”函數(shù)了,對于加密的PHP代碼(僅變形字符串,非zend等方式加密),肯定要用到“eval”函數(shù),所以,對于不管是用哪種加密方法的代碼,肯定要用到“eval”函數(shù)。其次就是可以執(zhí)行系統(tǒng)命令的函數(shù)了,比如上面某牛的代碼中提到的四個“system”、“shell_exec”、“exec”、“popen”。當然還有其他的,比如passthru等。PHP還支持“·”字符(ESC鍵下面那個)直接執(zhí)行系統(tǒng)命令。我們可以把正則寫成這樣【\b(?P<function>eval|proc_open|popen|shell_exec|exec|passthru|system)\b\s*\(】。

檢測PHP webshell的python腳本相對較為嚴謹?shù)钠ヅ?
解釋一下:
大家都知道【\b\b】用來匹配單詞兩邊的位置的。要保證【\b\b】中間的是單詞,即使函數(shù)名前面加特殊字符,也一樣通過匹配,比如加@來屏蔽錯誤。后面的【\s*】用來匹配空白字符的,包括空格,tab鍵,次數(shù)為0到無數(shù)次。前面的【(?P)】是捕獲命名組。用來當作python代碼直接引用匹配結(jié)果的key。
還有的網(wǎng)友提到了,如果我把代碼放到圖片拓展名的文件里呢?那你只檢測.php,.inc的文件,還是找不到我的呀。嗯,是的,如果惡意代碼在gif、jpg、png、aaa等亂七八糟的拓展名文件里,是不能被apache、IIS等web Services解析的,必須通過include/require(_once)來引入。那么,我們只要匹配include/require(_once)后面的文件名是不是常規(guī)的“.php”、“.inc”文件。如果不是,則為可疑文件。正則如下【(?P<function>\b(?:include|require)(?:_once)?\b)\s*\(?\s*["'](?P<filename>.*?(?<!\.(?:php|inc)))["']】。

檢測PHP WEBSHELL的python腳本較為嚴謹做法
解釋一下:
先看【(?P<function>\b(?:include|require)(?:_once)?\b)】,【(?P<name>)】為正則表達式的“命名捕獲”,PHP中有同樣的用法。也就是說,在這括號內(nèi)的捕獲的數(shù)據(jù),會分配到結(jié)果數(shù)組的key為“name”的value中。再看里面的【\b(?:include|require)(?:_once)?\b】,【\b\b】不解釋了,為單詞邊界位置。里面的【(?:include|require)】匹配字符串“include”、“require”兩個單詞,其中前面的【(?:)】未不分配組,用于提高效率,可以去掉【?:】變成【(include|require)】。在后面一個【(?:_once)】也是做不分配組的操作,便于提高正則表達式效率。同樣,后面的量詞是“?”代表這個組可有可無。就滿足了“include”、“include_once”、“require”、“require_once”四種情況。有的朋友可能這樣寫【(include|include_once|require|require_once)】也能實現(xiàn)目的。但是,為了更搞的效率,我們對這個正則做優(yōu)化,針對部分字符串做分支更改,改成上面那個【\b(?:include|require)(?:_once)?\b】。
再看下面的【\s*\(?\s*["'](?P<filename>.+?(?<!\.(?:php|inc)))["']】中,【\s*】匹配空白字符,包括空格,tab鍵等。后面的【\(?】,匹配字符“(”,后面的量詞“?”表示這半個小酷括號可有可無。防止“incude “123.php””這種沒有括號的情況。再后面【["']】匹配雙引號,單引號的。最后的也是。再看看這個【(?P<filename>.+?(?<!\.(?:php|inc)))】,其中【(?P<filename>)】上面介紹了,為命名捕獲,把結(jié)果放到match.group(“filename”)里。【.*?】為任意字符,后面的量詞是“忽略優(yōu)先量詞”,也就是平常說的“非貪婪”。這里最少匹配零個,(防止.aa、.htaccess這種沒有文件名,只有文件拓展名的文件被引入)。后面的【(?<!\.(?:php|inc))】,這里用到了反向零寬斷言(環(huán)視)的非操作(只匹配位置,不匹配字符串,跟【^$\b】等一樣)。這個表達式是針對這個位置的后面字符起作用的,也就是說后面的【["']】的前面不能是“.php”、“.inc”,這里也就是取了文件名的最后的拓展名。(正則里,可以用【^】對字符取非,但是不能對“字符串組”取非,這里用了零寬斷言來實現(xiàn)。)
綜上所述,最后,鄙人給出的python代碼如下:
復(fù)制代碼 代碼如下:
#!/usr/bin/python
#-*- encoding:UTF-8 -*-
###
## @package
##
## @author CFC4N <cfc4nphp@gmail.com>
## @copyright copyright (c) Www.cnxct.Com
## @Version $Id: check_php_shell.py 37 2010-07-22 09:56:28Z cfc4n $
###
import os
import sys
import re
import time
def listdir(dirs,liston='0'):
flog = open(os.getcwd()+"/check_php_shell.log","a+")
if not os.path.isdir(dirs):
print "directory %s is not exist"% (dirs)
return
lists = os.listdir(dirs)
for list in lists:
filepath = os.path.join(dirs,list)
if os.path.isdir(filepath):
if liston == '1':
listdir(filepath,'1')
elif os.path.isfile(filepath):
filename = os.path.basename(filepath)
if re.search(r"\.(?:php|inc|html?)$", filename, re.IGNORECASE):
i = 0
iname = 0
f = open(filepath)
while f:
file_contents = f.readline()
if not file_contents:
break
i += 1
match = re.search(r'''(?P<function>\b(?:include|require)(?:_once)?\b)\s*\(?\s*["'](?P<filename>.*?(?<!\.(?:php|inc)))["']''', file_contents, re.IGNORECASE| re.MULTILINE)
if match:
function = match.group("function")
filename = match.group("filename")
if iname == 0:
info = '\n[%s] :\n'% (filepath)
else:
info = ''
info += '\t|-- [%s] - [%s] line [%d] \n'% (function,filename,i)
flog.write(info)
print info
iname += 1
match = re.search(r'\b(?P<function>eval|proc_open|popen|shell_exec|exec|passthru|system)\b\s*\(', file_contents, re.IGNORECASE| re.MULTILINE)
if match:
function = match.group("function")
if iname == 0:
info = '\n[%s] :\n'% (filepath)
else:
info = ''
info += '\t|-- [%s] line [%d] \n'% (function,i)
flog.write(info)
print info
iname += 1
f.close()
flog.close()
if '__main__' == __name__:
argvnum = len(sys.argv)
liston = '0'
if argvnum == 1:
action = os.path.basename(sys.argv[0])
print "Command is like:\n %s D:\wwwroot\ \n %s D:\wwwroot\ 1 -- recurse subfolders"% (action,action)
quit()
elif argvnum == 2:
path = os.path.realpath(sys.argv[1])
listdir(path,liston)
else:
liston = sys.argv[2]
path = os.path.realpath(sys.argv[1])
listdir(path,liston)
flog = open(os.getcwd()+"/check_php_shell.log","a+")
ISOTIMEFORMAT='%Y-%m-%d %X'
now_time = time.strftime(ISOTIMEFORMAT,time.localtime())
flog.write("\n----------------------%s checked ---------------------\n"% (now_time))
flog.close()
## 最新代碼在文章結(jié)尾的鏈接里給出了。2010/07/31 更新。
僅供參考,歡迎斧正。
下面截圖為掃描Discuz7.2的效果圖,當然,也有誤報。相對網(wǎng)上流傳的python腳本,誤報更少,更精確了。

檢測PHP WEBSHELL的python腳本的檢測結(jié)果
問:這個方法完美了嗎?可以查找目前已知的所有危險函數(shù)文件了嗎?
答:不能,如果include等引入的文件沒有拓展名,這里就匹配不到了。
問:如何解決?
答:留給你解決,聰明的你,肯定可以搞定。
PS:“`”反引號 執(zhí)行命令的還沒寫,暫時沒好的辦法。容易跟SQL語句中的反引號混淆。不太好匹配。如果光匹配反引號就提示的話,那誤報太大了。待定吧。(術(shù)業(yè)有專攻,請勿因為一處不好的代碼,否定一個人的能力。你懂的。再次重申,此文只針對代碼,不針對人。其次,鄙人給出的python代碼隨便復(fù)制,隨便傳播,愛留版權(quán)就留版權(quán),不愛留就刪了相關(guān)字符,也就是您愛干嗎干嗎。)
我先休息一會,明天再說。(前半句為三國殺曹仁的臺詞,哈。)
相關(guān)文章
python數(shù)據(jù)結(jié)構(gòu):數(shù)據(jù)類型
這篇文章主要介紹了python數(shù)據(jù)結(jié)構(gòu)中的數(shù)據(jù)類型,在?Python?以及其他所有面向?qū)ο缶幊陶Z言中,類都是對數(shù)據(jù)的構(gòu)成(狀態(tài))以及數(shù)據(jù)?能做什么(行為)的描述,下面我們就來你看看python數(shù)據(jù)結(jié)構(gòu)中的數(shù)據(jù)類型商務(wù)詳細介紹,需要的小伙伴可以參考一下2021-12-12python 動態(tài)渲染 mysql 配置文件的示例
這篇文章主要介紹了python 動態(tài)渲染 mysql 配置文件的示例,幫助大家更好的理解和使用python,感興趣的朋友可以了解下2020-11-11python通過exifread模塊獲得圖片exif信息的方法
這篇文章主要介紹了python通過exifread模塊獲得圖片exif信息的方法,實例分析了Python操作exifread模塊的技巧,需要的朋友可以參考下2015-03-03Python測試網(wǎng)絡(luò)連通性示例【基于ping】
這篇文章主要介紹了Python測試網(wǎng)絡(luò)連通性,結(jié)合實例形式分析了Python通過發(fā)送ping請求測試網(wǎng)絡(luò)連通性相關(guān)操作技巧,需要的朋友可以參考下2018-08-08python實現(xiàn)linux服務(wù)器批量修改密碼并生成execl
這篇文章主要介紹了python實現(xiàn)linux服務(wù)器批量修改密碼并生成execl示例,需要的朋友可以參考下2014-04-04