客户服务器感染挖矿木马,分析后发现是利用Confluence RCE漏洞获取服务器权限,近几个月已经遇到了好几起利用Confluence RCE漏洞进而挖矿的事件,于是准备分析一波漏洞成因。
挖矿木马事件
1、在态势感知查看事件,发现有攻击者利用CVE-2021-26084漏洞进行攻击。
2、进入服务器中,查看进程该服务器存在定时计划任务,会向一个服务器下载脚本文件http://45.145.185.85/ldr.sh
3、服务器中存在恶意进程kthreaddi,可以确认为挖矿程序
4、在进程以及其它信息中可以发现更多挖矿病毒信息
5、通过开源情报信息和这些些特征信息做对比,明确该机器被Sysrv-hello僵尸网络所感染。通过对ip:45.145.185.85对威胁情报进行信息查询,发现该ip有Sysrv-hello僵尸网络标签,而且该服务器属于主控服务器,用于提供远程挖矿脚本服务。
漏洞复现
1、成功执行运算。
POST /pages/doenterpagevariables.action HTTP/1.1
Host: ip:8090
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 90
queryString=%5cu0027%2b%7b3*3%7d%2b%5cu0027
&linkCreation=%5cu0027%2b%7b3*4%7d%2b%5cu0027
/pages/doenterpagevariables.action
该接口是不需要登录的,有两个参数是可以利用的,一个是queryString
,一个是linkCreation
,初步来看两个的成因应该都是一样的
2、poc:https://github.com/r0ckysec/CVE-2021-26084_Confluence 代码执行成功
漏洞分析
近几个月已经遇到了好几起因为CVE-2021-26084而引发的挖矿木马事件,之前一直就想要分析一下,就趁这次机会使用debug模式对docker下的Confluence进行调试分析漏洞成因。
DEBUG教程
保姆式Confluence docker镜像DEBUG教程
拉取centos镜像
[root@localhost ~]# sudo docker pull centos
查看本地镜像
[root@localhost ~]# docker images
启动相应镜像
[root@localhost ~]# sudo docker run -d -p 8090:8090 -p 5050:5050 --privileged=true --name confluence_7.12.4_privileged 5d0da3dc9764 /usr/sbin/init
列出所有容器
[root@localhost ~]# docker container ls
CONTAINER IDIMAGE COMMAND CREATED STATUS PORTSNAMES
a6a5a6729cc55d0da3dc9764"/usr/sbin/init"4 minutes ago Up 3 minutes0.0.0.0:5050->5050/tcp, 0.0.0.0:8090->8090/tcp confluence_7.12.4_privileged
进入相应容器
[root@localhost ~]# sudo docker exec -it a6a5 bash
下载JDK
[root@a6a52ef8aaf8 /]# yum install java-11-openjdk-devel -y
设置JAVA_HOME
[root@a6a52ef8aaf8 /]# vim ~/.bashrc
添加到最后一行
JAVA_HOME=/usr/lib/jvm/java-11-openjdk-11.0.12.0.7-0.el8_4.x86_64
下载mysql
[root@a6a52ef8aaf8 /]# yum install -y mysql mysql-server
启动mysql服务
[root@a6a52ef8aaf8 /]# service mysqld start
进入数据库
[root@a6a52ef8aaf8 /]# mysql
创建数据库
mysql> create database confluence;
设置编码
mysql> ALTER DATABASE confluence CHARACTER SET utf8 COLLATE utf8_bin;
设置事务隔离级别
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
更改事务隔离级别
[root@a6a52ef8aaf8 bin]# vim /etc/my.cnf
将下面这段话添加到文件末尾
transaction-isolation=READ-COMMITTED
重启mysql服务
[root@a6a52ef8aaf8 bin]# service mysqld restart
下载Confluence
[root@a6a52ef8aaf8 /]# wget https://product-downloads.atlassian.com/software/confluence/downloads/atlassian-confluence-7.12.4.tar.gz
解压到opt路径下
[root@a6a52ef8aaf8 /]# tar -xzvf atlassian-confluence-7.12.4.tar.gz -C /opt/
更改home目录
[root@a6a52ef8aaf8 atlassian-confluence-7.12.4]# vim confluence/WEB-INF/classes/confluence-init.properties
将下面这段话添加到文件末尾
confluence.home=/opt/confluence/data
添加debug端口
[root@a6a52ef8aaf8 atlassian-confluence-7.12.4]# cd bin
[root@a6a52ef8aaf8 bin]# vim setenv.sh
将下面这段话添加到文件倒数第二行
CATALINA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5050 ${CATALINA_OPTS}"
下载mysql驱动
[root@a6a52ef8aaf8 lib]# wget https://cdn.mysql.com//Downloads/Connector-J/mysql-connector-java-8.0.26.tar.gz
解压添加到lib目录
[root@a6a52ef8aaf8 lib]# tar -xzvf mysql-connector-java-8.0.26.tar.gz -C /atlassian-confluence-7.12.4/lib/
运行启动脚本
[root@a6a52ef8aaf8 atlassian-confluence-7.12.4]# cd bin
[root@a6a52ef8aaf8 bin]# sh startup.sh
访问ip:8090
申请试用
数据库选择mysql,地址172.0.0.1,数据库confluence,账号root,密码为空
confluence创建完成
将源代码从容器中复制
[root@localhost ~]# docker cp a6a52ef8aaf8:/opt/atlassian-confluence-7.12.4/confluence confluence
然后传到本地,使用IDEA打开
配置remote Host为服务器IP。 Port为5050端口。
将WEB-INF目录下的lib、atlassian-bundled-plugins、atlassian-bundled-plugins—setup加入依赖库
官方修复
官网关于漏洞的通报https://confluence.atlassian.com/doc/confluence-security-advisory-2021-08-25-1077906215.html 下载漏洞补丁
# File 1 of 5
sed $SEDFLAGS 's/(Enable dark feature.+value=)[^"]+"/\1featureKey"/' confluence/users/user-dark-features.vm;
# ######################################
# File 2 of 5
sed $SEDFLAGS 's/("Hidden" "name=.token." "value=)[^"]+"/\1token"/' confluence/login.vm;
# ######################################
# File 3 of 5
sed $SEDFLAGS 's/("Hidden" "name=.([a-zA-Z]+)." "value=).\$[!l][^"]+"/\1\2"/' confluence/pages/createpage-entervariables.vm;
# ######################################
# File 4 of 5
sed $SEDFLAGS 's/("Hidden" "name=.([a-zA-Z]+)." "value=).\$[!l][^"]+"/\1\2"/' confluence/template/custom/content-editor.vm;
sed $SEDFLAGS 's/("Hidden" "id=sourceTemplateId.*value=)[^"]+"/\1templateId"/' confluence/template/custom/content-editor.vm;
# ######################################
# File 5 of 5
sed $SEDFLAGS 's/("Hidden" "id=syncRev.*value=)[^"]+"/\1syncRev"/' $TMP_EXTRACT_DIR/templates/editor-preload-container.vm;
修改取消了所有漏洞点参数中的$符号,程序不会再对前端获取到的值进行Ognl表达式计算,也就不存在Ognl表达式注入导致命令执行的漏洞了。
confluence/users/user-dark-features.vm
value='$!action.featureKey' -> value=featureKey
confluence/login.vm
value='$!action.token' -> value=token
confluence/pages/createpage-entervariables.vm
重点重点重点!!!
#tag ("Hidden" "name='queryString'" "value='$!queryString'") -> #tag ("Hidden" "name='queryString'" "value=queryString")
#tag ("Hidden" "name='linkCreation'" "value='$linkCreation'") -> #tag ("Hidden" "name='linkCreation'" "value=linkCreation")
confluence/template/custom/content-editor.vm
将多个value值的$!删除
confluence/WEB-INF/atlassian-bundled-plugins/confluence-editor-loader*.jar中的templates/editor-preload-container.vm
value='$!{action.syncRev}' -> value=syncRev
OGNL
先简单了解一下什么是OGNL
OGNL具有三要素:expression、根对象root和context。
expression:expression表达式告诉OGNL需要执行什么操作;
root:OGNL可以对root进行取值或写值等操作,
context:context可以理解为对象运行的上下文环境,context以MAP的结构、利用键值对关系来描述对象中的属性以及值;
OGNL表达式注入
Ognl 的getValue 和 setValue
都可以用来执行代码
OGNL表达式的getValue()
解析过程就是先将整个OGNL表达式按照语法树分为几个子节点树,然后循环遍历解析各个子节点树上的OGNL表达式,其中通过Method.invoke()
即反射的方式实现任意类方法调用,将各个节点解析获取到的类方法通过ASTChain
链的方式串连起来实现完整的表达式解析、得到完整的类方法调用。
简单地说,在getValue
中形如 ()(a)(b)
的表达式在 ognl 处理的时候会先计算()(a)
,然后返回带有 payload 的 ASTEval
树,再以b
为root
计算 AST
树,最终执行命令。
常用 paylaod:
//获取context里面的变量
#user
#user.name
//使用runtime执行系统命令
@java.lang.Runtime@getRuntime().exec("calc")
//使用processbuilder执行系统命令
(new java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).start()
//获取当前路径
@java.lang.System@getProperty("user.dir")
分析调用
看到OGNL表达式注入,就找xwork-core-*.jar
,然后去寻找getValue()
函数,我们在Confluence的xwork-core-*.jar
包中找到了两个文件中存在三个getValue()
函数
我们给这三处打上断点进行调试,发现最终停在了OgnlValueStack.findValue()处。
这里简单调试分析下Ognl.getValue()
解析OGNL表达式的过程。
在Ognl.getValue
代码处打下断点,往下调试,调用了OgnlUtil.compile(expr)
进入到OgnlUtil.compile(expr)
中,看到调用了parseExpression()
函数,该函数将传入的String类型的字符串解析为OGNL表达式能理解的ASTConst
类型:
往下,将传入的ASTConst
类型的tree参数转换成Node类型(ASTConst继承自SimpleNode、SimpleNode继承自Node
)再调用其getValue()
函数继续解析:
由于tree变量就是表达式解析来的东西,因此接下来的调用中局部环境中的this变量的值就是我们的OGNL表达式的内容。往下就是调用的SimpleNode.getValue()
函数,其中调用了evaluateGetValueBody()
函数:
evaluateGetValueBody()
函数,用于计算getValue体中OGNL表达式的值。跟进看是直接调用了getValueBody()
函数
在SimpleNode中会对node节点进行处理,这里中间就会发生OGNL表达式注入问题,然后传入AbstractTagDirective中,关于OGNL表达式注入的调试就先结束了。
Velocity
1、Velocity是什么
Debug完成后,根据公开的payload,进入idea搜索一下doenterpagevariables.action
,看到是在pages/createpage-entervariables.vm
下
*.vm 后缀的文件,是velocity的文件。
velocity是基于java的一种页面模板引擎,支持#if #else #foreach等写法的前台文件。
Velocity 的工作原理:
$开头指引用,表明一个变量或对象。
#开头指脚本语句
!一般和$一起使用,表明变量没有值时,将输出一个空字符串
{}表示声明一个velocity变量
例如:当表单加载并$email仍然没有值时,将输出一个空字符串。
<input type="text" name="email" value="$!{email}"/>
例如:Hello world
<html>
<正文>
#set( $foo = "Hello" )
$foo world!
</正文>
</html>
2、#tag
Velocity 中的#tag用于生成“输入”标签,例如
#tag ("Hidden" "name='queryString'" "value='$!queryString'")
html 输出将是:
<input type=" hidden " name=" queryString " value="<value>"/>
#tag的用法大致是这样的:
#tag (“attribute1” “attribute2” “attribute3”)
期间#tag的这些属性将在AbstractTagDirective 中处理
3、分析调用
在上面OGNL调试中,我们调试到了AbstractTagDirective
,我们继续。
在AbstractTagDirective
中首先调用applyAttributes(contextAdapter, node, object)
处理参数,其中AbstractTagDirective.createPropertyMap()
创建Map,保存键值对。
然后AbstractTagDirective.render()
调用processTag()
去处理#tag
AbstractTagDirective.applyAttributes()
会进入到AbstractUITag
中,AbstractUITag.doEndTag()
去调用 AbstractUITag.evaluateParams()
获取value值
这个value值会被传递到WebWorkTagSupport.findValue()
中,然后会以expr(表达式)的形式被传递到OgnlValueFinder.findValue()
中,然后调用SafeExpressionUtil.isSafeExpression()
进行安全检查。
进入到isSafeExpression()
中,该方法会通过containsUnsafeExpression()
处理表达式
我们先不管过滤机制,继续跟踪发现进入到了OgnlValueStack.findValue()
中,最终调用Ognl.getValue
计算表达式,就是这里引起了OGNL表达式注入
我们上面用的payload是测试
,然后最终到Ognl.getValue
计算表达式里面是"'测试'"
,那么使用payload'测试'->"''测试''"
,不就可以造成OGNL表达式注入
了
但是当我使用'
的时候,我发现被转义为 html 实体形式,成为了'
在HtmlAnnotationEscaper
中调用HtmlEntities
被转义
那我们为什么可以使用“ \u0027
”来进行绕过呢?
因为在OgnlParserTokenManager.getNextToken()
中的 “this.input_stream.readChar()
”会读取1个字符然后返回,当遇到“\u
”时,JavaCharStream.readChar()
将其转换为字符,所以可以使用“ \u0027
”来进行绕过html转义
然后我们的payload只需要再绕过静态方法SafeExpressionUtil.isSafeExpression
的黑名单安全检查就可以了
黑名单进行了一个简单的字符串匹配,
- 第一个hashset限制了构造函数的关键字
- 第二三个hashset限制了获取classloader
- 第四个hashset限制了编译后的结果中不能出现特定的变量
那么我们就可以使用类似数组的运算符来绕过这里绕过方式可以使用数组进行绕过,
只是改变:
"".getClass().forName("java.lang.Runtime")
为
""["class"].forName("java.lang.Runtime")
就可以绕过黑名单机制了
总结:
Confluence 远程代码执行漏洞的过程基本如下
- 本文作者: 苏苏的五彩棒
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/961
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!