先來看下反引號可以成功執行命名的程式碼片段。代碼如下:
複製代碼 代碼如下:`ls -al`;
`ls -al`;
echo "sss"; `ls -al`;
$sql = "SELECT `username` FROM `table` WHERE 1";
$sql = 'SELECT `username` FROM `table` WHERE 1'
/*
無非是 前面有空白字元,或者在一行代碼的結束之後,後面接著寫,下面兩行為意外情況,也就是SQL命令裡的反引號,要排除的就是它。
*/
Regex該如何寫?
分析:
對於可移植性的部分共同點是什嗎?與其他正常的包含反引號的部分,區別是什嗎?
他們前面可以有空格,tab鍵等空白字元。也可以有程式碼,前提是如果有引號(單雙)必須是閉合的。才是危險有隱患的。遂CFC4N給出的正則如下:【(?:(?:^(?:\s+)?)|(?:(?P<quote>["'])[^(?P=quote)]+?(?P=quote)[^`]*?))`(?P<shell>[^`]+)`】。
解釋一下:
【(?:(?:^(?:\s+)?)|(?:(?P<quote>["'])[^(?P=quote)]+?(?P=quote)[^`]*?))】匹配開始位置或者開始位置之後有空白字元或者前面有代碼,且代碼有閉合的單雙引號。(這段PYTHON的正則中用了捕獲命名以及反向引用)
【`(?P<shell>[^`]+)`】這個就比較簡單了,匹配反引號中間的字串。
python指令碼檢測PHP WEBSHELL
然後我將這段代碼寫入程式中,測試跑了一下discuz的程式。結果有一個誤判。誤判的位置為“config.inc.php”中的“define(‘UC_DBTABLEPRE', ‘`ucenter`.uc_');”,什麼原因造成的?這行代碼符合了前面有閉合的引號,也有反引號的使用,所以,符合要求,被檢測到了。如何再排除這種情況呢?這個有什麼特殊的?前面有逗號“,”?如果是字串串連的點號“.”呢?再排除逗號?
好吧,我錯了,我不該用我的思維來誤導你。換個思路。找下反引號可執行檔代碼的前面字串的情況,他們只能是行的開始,或者有空白字元(包括空格,tab鍵等),再前面也可以有代碼的結束標識分號“;”,其他的情況,都是不可以執行的吧?嗯,應該是這樣。(如有錯誤,歡迎斧正)既然思路有了,那正則代碼更好寫了。如下【(^|(?<=;))\s*`[^`]+`】,解釋一下,【(^|(?<=;))】匹配位置,是行的開始,或者前面有分號“;”。【\s*`[^`]+`】空白字元任一個,然後是….(你懂的)。OK,寫好之後,檢測,又發現一個問題。
匹配引入檔案的正則也匹配了“require_once ‘./include/db_'.$database.'.class.php';”這種代碼,什麼原因造成的,您自己分析吧。
給出修複之後的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)))["']\)?\s*;''', 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
match = re.search(r'(^|(?<=;))\s*`(?P<shell>[^`]+)`\s*;', file_contents, re.IGNORECASE)
if match:
shell = match.group("shell")
if iname == 0:
info = '\n[%s] :\n'% (filepath)
else:
info = ''
info += '\t|-- [``] command is [%s] in line [%d] \n'% (shell,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()
稍微檢測了一下Discuz7.2的代碼,還是有誤判的,誤判的為這種包含sql的代碼: 複製代碼 代碼如下:$query = $db->query("SELECT `status`,`threads`,`posts`
FROM `{$tablepre}forums` WHERE
`status`='1';
");
稍微檢測了一下Discuz7.2的代碼,還是有誤判的,誤判的為這種包含sql的代碼: 複製代碼 代碼如下:$query = $db->query("SELECT `status`,`threads`,`posts`
FROM `{$tablepre}forums` WHERE
`status`='1';
");
由於這個指令碼是按照一行一行的代碼來處理的,所以,有這種誤判。您自己去修複吧。相對網上流傳的指令碼來說,還是比較準確的。
歡迎轉載。轉載請註明來源,以及留下部落格連結,同時,不能用於商業用途。(已經修複,增加了反引號後面【\s*;】的判斷。2010-07-27 17:06)
PS:如果說上傳檔案也算是危險的、值得注意的操作的話,建議加上move_uploaded_file函數的檢測。你知道在哪裡加的。^_^
2010-12-17 關於這些代碼,已經放到google 的代碼託管上了。SVN地址為 http://code.google.com/p/cnxct/ 大家個獲得最新版。
我是一個PHPer,寫的python有點憋,有點懶,還請各位安全界的大牛,程式界的前輩不要鄙視,要給建議,謝謝。php版的以後在寫吧。同時,也歡迎各位安全愛好者反饋最新的web shell特徵代碼,我儘力增加到程式中區。
完整的代碼複製代碼 代碼如下:#!/usr/bin/python
#-*- encoding:UTF-8 -*-
###
## @package
##
## @author CFC4N <cfc4nphp@gmail.com>
## @copyright copyright (c) Www.cnxct.Com
## @Version $Id$
###
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)))["']\)?\s*;''', 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|assert|fwrite|create_function)\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
match = re.search(r'(^|(?<=;))\s*`(?P<shell>[^`]+)`\s*;', file_contents, re.IGNORECASE)
if match:
shell = match.group("shell")
if iname == 0:
info = '\n[%s] :\n'% (filepath)
else:
info = ''
info += '\t|-- [``] command is [%s] in line [%d] \n'% (shell,i)
flog.write(info)
print info
iname += 1
match = re.search(r'(?P<shell>\$_(?:POS|GE|REQUES)T)\s*\[[^\]]+\]\s*\(', file_contents, re.IGNORECASE)
if match:
shell = match.group("shell")
if iname == 0:
info = '\n[%s] :\n'% (filepath)
else:
info = ''
info += '\t|-- [``] command is [%s] in line [%d] \n'% (shell,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()