エクスプロイト&脆弱性
Ivanti Avalancheにおける任意ファイルアップロード関連の脆弱性「CVE-2023-46263」について
Ivanti Avalancheに存在し、悪用されると任意のファイルのアップロードが可能となるリモートコード実行関連の脆弱性が報告されました。
この脆弱性は悪用されると、特別に作成されたリクエストを認証済の攻撃者がリモートから対象となるサーバに送信することで、システム権限でのリモートコード実行が可能となります。
脆弱性「CVE-2023-46263」について
Ivanti Avalancheは、モバイルデバイスを管理するためのシステムであり、このシステム内の「Central FileStore」と「Central File Server」は、モバイルデバイスの設定に必要なファイルを保管し、配布する役割を担っています。例えば、アプリケーションの.apkファイルやOSのアップデートファイルが「Central FileStore」に保存されることがあります。この脆弱性「CVE-2023-46263」を完全に理解するには、「Central FileStore」について知ることが重要となります。
Avalancheのウェブインターフェースは、TCPポート8080を通じてHTTPでアクセス可能です。具体的には、以下の方法でアクセスできます:
http://:8080/AvalancheWeb/login.jsf
HTTPは、RFC 7230から7237までのRFCおよびその他のRFCで記述されたリクエスト/レスポンスプロトコルです。クライアントからサーバへリクエストが送信され、その後サーバからクライアントへレスポンスが返送されます。以下のとおり、HTTPリクエストは、リクエストライン、さまざまなヘッダ、空行、およびオプションのメッセージボディで構成されます。
Request = Request-Line headers CRLF [message-body]
Request-Line = Method SP Request-URI SP HTTP-Version CRLF
Headers = *[Header]
Header = Field-Name ":" Field-Value CRLF
CRLFは、キャリッジリターン(CR)とラインフィード(LF)の改行シーケンスを指します。SPはスペースの文字です。パラメータは、使用するメソッドやContent-Typeヘッダに応じて、リクエストURIやメッセージ本文の中で名前と値の組み合わせでクライアントからサーバへ渡すことができます。例えば、GETメソッドを使って「param」という名前のパラメータに「1」という値を渡す簡単なHTTPリクエストは、以下のようになります。
GET /my_webapp/mypaget.htm?param=1 HTTP/1.1 Host: www.myhost.com
POSTメソッドを使用した対応するHTTPリクエストは以下のようになります。
POST /my_webapp/mypage.htm HTTP/1.1
Host: www.myhost.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 7
param=1
パラメータ/値のペアが複数ある場合、それらは、以下のとおり、「&」で区切られたname=valueのペアとしてエンコードされます。
var1=value1&var2=value2...
Avalancheを使用すると、ユーザはウェブインターフェースからFileStoreの保存パスを変更でき、これによってCentral FileStoreがファイルを保存する位置を調整することが可能です。パス変更は、以下へのリクエスト送信によって行われ、
AvalancheWeb/app/FileStoreConfig.jsf
このリクエストは、以下のクラスで処理されます。
com.wavelink.amc.web.view.FileStoreConfigBean
このリクエストには、ファイルを保存する新しいパスを指定するパラメータtxtUncPathが含まれています。そして新しいパスを保存する前に以下のメソッドを使用して、新しいパスが許可されているかどうかを確認します。
validateFileStoreUncPath
そして新しいパスは、許可されていない値のリストとディレクトリトラバーサル文字に対して検証されます。チェックをクリアした場合、新しいパスは保存され、今後FileStoreにアップロードされるファイルは新しい場所に保存されます。
そしてCentral FileStoreには任意のファイルアップロードの脆弱性が存在しています。この問題は、Central FileStoreの設定内のフィールドtxtUncPathが適切にサニタイズされていないために発生します。
validateFileStoreUncPath
このメソッド機能は、新規パスがAvalancheサーバのwebrootフォルダを含まないようにすることを目的としていますが、以下に位置するRemoteControlサーバのフォルダwebrootの親フォルダの使用は回避しません。
C:\ProgramData\Wavelink\Avalanche\RemoteControlServer\app
攻撃者は、txtUncPathの値を以下に設定することで、禁止されたパスのチェックを回避します。
C:\ProgramData\Wavelink\Avalanche
そして以下のサブフォルダに不正なファイルをアップロードするリクエストを送信することが可能となります。
RemoteControlServer\app
RemoteControlサーバは通常、Windows Mobile/CEデバイスを制御するために使用され、以下へのHTTPリクエストでアクセスできます。
http://<hostname>:1900/
デフォルト設定では、RemoteControlサーバはVelocityマクロコードを実行します。攻撃者がRemoteControlサーバのwebrootに特別に作られたファイルをアップロードすると、システム上で任意のコマンドを実行する能力を得る可能性があります。
ソースコードの解説
以下に示すコードのスニペットは、Ivanti Avalancheのバージョン6.4.1から抜粋したものです。トレンドマイクロが追加したコメントは、強調して表示されています。
AvalancheWeb.jar内の以下からの次のようなコードとなります。
app/FileStoreConfigSettings.xhtml
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui">
<style>
.dialogTree .ui-dialog-content.ui-widget-content {
padding: 0;
background: none;
}
[ ...truncated for readability... ]
<div class="inputLbl">#{bundle.filestoreConfig_sharedmapping_unc_path_lbl}</div>
<p:inputText
value="#{fileStoreConfig.fileStoreUncPath}"
disabled="#{!userCheck.rootAdmin}"
size="80"
// Call validateFileStoreUncPath on txtUncPath parameter
id="txtUncPath"
validator="#{fileStoreConfig.validateFileStoreUncPath}"
styleClass="txtField"
maxlength="255"/>
[ ...truncated for readability... ]
AvalancheWeb.jar内の逆コンパイルされた以下のクラスからの次のようなコードとなります。
WEB-INF/classes/com.wavelink.amc.web.view.CentralFileStoreDialog
@ManagedBean(name = "fileStoreConfig")
@SessionScoped
public class FileStoreConfigBean extends AbstractViewBean implements Serializable {
@ManagedProperty("#{pageFlowScope}")
protected PageFlowScope pageFlowScope;
private FileStoreConfigPayload m_currentConfig;
private String m_unc;
private String m_uname;
private String m_upass;
private String m_cert;
private String m_certShortFileName;
private String m_certpass;
private String m_serverUrl;
private String m_velocityFolder;
private String m_velocityCertHash;
private String m_defaultunc;
private boolean m_configSaved = false;
private String m_notAfter;
public void setPageFlowScope(PageFlowScope pageFlowScope) {
this.pageFlowScope = pageFlowScope;
}
private static final Logger logger = LogManager.getLogger(
com.wavelink.amc.web.view.FileStoreConfigBean.class);
// List of disallowed paths does not include RemoteControl server webroot parent folders
private static final String[] g_exclPatterns = new String[] { ".*?/AvalancheWeb.*", ".*?/webapps.*",
".*?/Users.*", ".*?/Program Files.*", ".*?/Program Files (x86).*", ".*?/AVALAN~1.*", ".*?/PROGRA~1.*",
".*?/PROGRA~2.*", ".*?/Windows.*", ".*?/RemoteControlServer.*" };
private List<Pattern> m_exclusionPatterns = new ArrayList<>();
public String onLoad() {
logger.trace("FileStoreConfigBean.onLoad()");
init();
return "viewFileStoreConfig";
}
@PostConstruct
public void init() {
// Generate list of excluded paths
generatePatternList(g_exclPatterns, this.m_exclusionPatterns);
getCurrentFileStoreConfig();
if (this.m_currentConfig == null) {
logger.error("No current FileStore configuration");
} else {
setSavedFields();
}
}
[ ...truncated for readability... ]
public void validateFileStoreUncPath(FacesContext context, UIComponent component,
Object value) throws ValidatorException {
logger.trace("FileStoreConfigBean.validateFileStoreUncPath()");
// Attacker controlled path value
String uncPath = value.toString();
if (uncPath.isEmpty())
return;
String testPath = uncPath.replace("\\", "/");
String default63Path = this.m_default63unc.replace("\\", "/");
String default64Path = this.m_default64unc.replace("\\", "/");
String currentPath = this.m_unc.replace("\\", "/");
if (testPath.contains(".."))
throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
BundleManager.getBundleString("inputError", "file_store_unc_path_parent"), null));
if (testPath.contains("./") || testPath.contains("/."))
throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
BundleManager.getBundleString("inputError", "file_store_unc_path_current"), null));
if (testPath.endsWith("/"))
throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
BundleManager.getBundleString("inputError", "file_store_unc_path_trailer"), null));
// Attempt to exclude paths but does not account for parent folder of webroot folder
for (Pattern patt : this.m_exclusionPatterns) {
Matcher matcher = patt.matcher(testPath);
if (matcher.find()) {
if (testPath.equalsIgnoreCase(default63Path) && testPath.equalsIgnoreCase(currentPath)) {
logger.debug(String.format("Filestore path '%s' is forbidden but tolerated because
it is the default CFS path", new Object[] { uncPath }));
continue;
}
logger.error(String.format("Filestore path '%s' is forbidden", new Object[] { uncPath }));
logger.info("ProposedPath: " + testPath);
logger.info("Default63Path: " + default63Path);
logger.info("Default64Path: " + default64Path);
logger.info("CurrentPath: " + currentPath);
throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
BundleManager.getBundleString("inputError", "file_store_unc_path_forbidden"), null));
}
}
boolean uncStart = uncPath.startsWith("\\");
boolean dosStart = (uncPath.length() > 1 && uncPath.charAt(1) == ':' && Character.isLetter(uncPath.charAt(0)));
if (!uncStart && !dosStart)
throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
BundleManager.getBundleString("inputError", "file_store_unc_path"), null));
if (!isPathValid(uncPath))
throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
BundleManager.getBundleString("inputError", "file_store_unc_path_syntax"), null));
}
この脆弱性を利用した攻撃の検出ガイド
この脆弱性を利用した攻撃を検出するためには、監視装置がTCPポート8080(HTTP用)および8443(HTTPS用)でのトラフィックを監視し分析する必要があります。トラフィックがSSLで暗号化されている場合があるため、注意が必要です。そのため、次の手順に進む前に、監視装置でトラフィックの暗号を解除することが求められる場合があります。
検出装置は、次のパスを含むリクエストURIに対する全てのHTTP POSTリクエストを監視する必要があります。
/AvalancheWeb/app/FileStoreConfig.jsf
リクエストが検出された場合、検出装置はリクエスト本体内で以下のパラメータを探し出す必要があります。
linkFileStoreConfigSave
このパラメータの値が同じ文字列である場合、次に以下のパラメータの値を探し出す必要があります。
txtUncPath
そしてそこに以下の文字列が含まれているかを検証します。
ProgramData\Wavelink\Avalanche
この文字列が含まれている場合、そのリクエストは、この脆弱性を悪用する攻撃が行われている可能性が高いため、疑わしいものと判断すべきです。以下は、不正なリクエストの例となります。
POST /AvalancheWeb/app/FileStoreConfig.jsf HTTP/1.1 Accept-Encoding: identity
Content-Length: 661
Host: 172.16.20.98:8080
Cookie: <cookie>
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux_x86_64; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Origin: http://172.16.20.98:8080/
Referer: http://172.16.20.98:8080/AvalancheWeb/app/inventory.jsf
Content-Type: application/x-www-form-urlencoded
Faces-Request: partial/ajax
X-Requested-With: XMLHttpRequest
Connection: close
javax.faces.partial.ajax=true&
javax.faces.source=linkFileStoreConfigSave& javax.faces.partial.execute=linkFileStoreConfigSave formFileStoreConfig& javax.faces.partial.render=messages txtUncPath txtUname txtUpass txtCertificateFileName txtCertPass txtServerUrl txtVelocityFolder txtVelocityCertHash& linkFileStoreConfigSave=linkFileStoreConfigSave& formFileStoreConfig=formFileStoreConfig&
txtUncPath=C:\ProgramData\Wavelink\Avalanche&
txtUname=&
txtUpass=&
txtCertificateFileName=&
txtCertPass=&
txtServerUrl=https://172.16.20.98:9000&txtVelocityFolder=& javax.faces.ViewState=-8991943161012586899:2086980044940355964
結論
Ivantiは、バージョン6.4.2のリリースにより、この脆弱性を含む複数の不具合に対する修正パッチが提供されました。他の対策方法は示されていないため、Ivanti Avalancheの利用者は、今回の脆弱性を完全に解決するために、この修正パッチをテストし、適用することを推奨します。
トレンドマイクロが運営するバグバウンティコミュニティ、Zero Day Initiativeの脅威リサーチチームは、今後も注目すべき脆弱性の分析レポートを提供していきます。エクスプロイト技術やセキュリティパッチに関する最新情報については、Twitter、Mastodon、LinkedIn、Instagramなどのサイト(英語)もご確認ください。
参考記事:
CVE-2023-46263: IVANTI AVALANCHE ARBITRARY FILE UPLOAD VULNERABILITY
By: Trend Micro Research Team
翻訳:与那城 務(Core Technology Marketing, Trend Micro™ Research)