前言
某日逛cve,发现一个后台的rce,挺感兴趣就跟了一下,记录一些当时的过程。
概况
参考https://hackerone.com/reports/1102067的说法,这是一个Authenticated path traversal to RCE,就还是路径的问题,导致后台在渲染页面的时候包含了可以通过路径穿越抵达的任意文件,造成了rce。
通过描述,bFilename参数是在editing layout design过程中可以通过抓包工具进行赋值,在后台渲染页面返回给前端的时候包含了该文件,前提要通过后台上传一个图片格式文件(内含php马),拿到返回路径后通过../../的格式设置路径,从而达到包含。
主要记录的原因
这里我复现完之后想看下代码的主要原因在于,渲染过程是如何调用到通过bFilename设置的文件,顺带想想自己如何通过整个流程,拓宽一些代码审计的思路。
简单分析
一是设置bFilename:
观察save layout design的请求,传递了一些重要的参数,bID和cID等,(因为部分截图在记录前,所以实际有出入)并且post bFilename,
POST /index.php/ccm/system/dialogs/block/design/submit?ccm_token=1650357492:7676740d3352aabc79c4f5a0be7581d0&cID=230&arHandle=Main&bID=195 HTTP/1.1
Host: www.concretetest.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 383
Origin: http://www.concretetest.com
Connection: close
Referer: http://www.concretetest.com/index.php?cID=230&ctask=check-out-first&ccm_token=1650357434:13fd818e280cd778e785055e257ed9e6
Cookie: CONCRETE5=0cba5r25gampmlu1glq65fgmk7; CONCRETE5_LOGIN=1; ConcreteSitemapTreeID=1
bFilename=&textColor=&linkColor=&alignment=&backgroundColor=&backgroundImageFileID=0&backgroundRepeat=no-repeat&backgroundSize=auto&backgroundPosition=left+top&borderColor=&borderStyle=&boxShadowColor=&hideOnDevice%5B10%5D=0&hideOnDevice%5B20%5D=0&hideOnDevice%5B30%5D=0&hideOnDevice%5B40%5D=0&customID=&customElementAttribute=&enableBlockContainer=-1&__ccm_consider_request_as_xhr=1
通过路由请求到后台的concrete/controllers/dialog/block/design.php:36 submit()
简单分析一下(多的也不会):38行鉴权,通过47行获取到了传递进来的参数,68行bFilename进入了$data数组,紧接着调用updateBlockInformation($data)
看注释,这是一个数据库更新的操作,通过bID进行更新,通过这一步就将文件路径写入到数据库中。(文件可以通过后台任意上传功能上传)
二是渲染执行的过程:
也没有从头开始跟,因为最后是include,所以我就直接把上传的文件改成php后缀,然后在代码执行处打上断点,便直接开始了:
因为返回的过程中只传入了一个参数(后面分析得到也就是cID),这里想看看最后是如何引用bFilename指定的文件。
请求site/index.php/!drafts/229中,在concrete/src/Http/Middleware/DispatcherDelegate.php:39会对请求进行处理:
concrete/src/Http/DefaultDispatcher.php:59进一步处理请求:
跟进concrete/src/Http/DefaultDispatcher.php handleDispatch:130:
concrete/src/Page/Page.php getFromRequest:476数据库查询语句:
$row = $db->fetchAssoc('select pp.cID, ppIsCanonical from PagePaths pp inner join Pages p on pp.cID = p.cID where cPath = ? and siteTreeID in (' . $treeIDs . ')', [$path]);
$path通过460行返回,该查询语句实现从请求到返回cID。
然后看返回页面的过程,
从concrete/src/Http/DefaultDispatcher.php handleDispatch:131进入,这里列出一些关键地方,从concrete/src/View/View.php:288 renderTemplate函数开始,先是包含了 concrete/themes/elemental/default.php,进行一些页面头文件的渲染,之后9行进入正题:
这里的$c根据前面的分析,已经带入了cID,跟进到11行:
load:771中调用了getAreaBlocks:
getBlocks:
跟进getBlockIDs:
/** * List the block IDs and the associated area handles in the currently loaded collection version (or in a specific area within it). * * @param string|false $arHandle The handle if the area (or falsy to get all the blocks in the collection version) * * @return array Return a list of arrays, each one is a dictionary like ['bID' => <block ID>, 'arHandle' => <area handle>] */ public function getBlockIDs($arHandle = false) { $blockIDs = CacheLocal::getEntry( 'collection_block_ids', $this->getCollectionID() . ':' . $this->getVersionID() ); if (!is_array($blockIDs)) { $v = [$this->getCollectionID(), $this->getVersionID()]; //cID $db = Loader::db(); $q = 'select Blocks.bID, CollectionVersionBlocks.arHandle from CollectionVersionBlocks inner join Blocks on (CollectionVersionBlocks.bID = Blocks.bID) inner join BlockTypes on (Blocks.btID = BlockTypes.btID) where CollectionVersionBlocks.cID = ? and (CollectionVersionBlocks.cvID = ? or CollectionVersionBlocks.cbIncludeAll=1) order by CollectionVersionBlocks.cbDisplayOrder asc'; $r = $db->GetAll($q, $v); $blockIDs = []; if (is_array($r)) { foreach ($r as $bl) { $blockIDs[strtolower($bl['arHandle'])][] = $bl; } } CacheLocal::set('collection_block_ids', $this->getCollectionID() . ':' . $this->getVersionID(), $blockIDs); } $result = []; if ($arHandle != false) { $key = strtolower($arHandle); if (isset($blockIDs[$key])) { $result = $blockIDs[$key]; } } else { foreach ($blockIDs as $arHandle => $row) { foreach ($row as $brow) { if (!in_array($brow, $blockIDs)) { $result[] = $brow; } } } } return $result; }
这里通过cID关联到了bID,接着,getBlocks函数的894:
foreach ($blockIDs as $row) { $ab = Block::getByID($row['bID'], $this, $row['arHandle']);
concrete/src/Block/Block.php:168,getByID函数的数据库查询:
$q = 'select CollectionVersionBlocks.isOriginal, CollectionVersionBlocks.cbIncludeAll, Blocks.btCachedBlockRecord, BlockTypes.pkgID, CollectionVersionBlocks.cbOverrideAreaPermissions, CollectionVersionBlocks.cbOverrideBlockTypeCacheSettings, CollectionVersionBlocks.cbRelationID, CollectionVersionBlocks.cbOverrideBlockTypeContainerSettings, CollectionVersionBlocks.cbEnableBlockContainer, CollectionVersionBlocks.cbDisplayOrder, Blocks.bIsActive, Blocks.bID, Blocks.btID, bName, bDateAdded, bDateModified, bFilename, btHandle, Blocks.uID from CollectionVersionBlocks inner join Blocks on (CollectionVersionBlocks.bID = Blocks.bID) inner join BlockTypes on (Blocks.btID = BlockTypes.btID) where CollectionVersionBlocks.arHandle = ? and CollectionVersionBlocks.cID = ? and (CollectionVersionBlocks.cvID = ? or CollectionVersionBlocks.cbIncludeAll=1) and CollectionVersionBlocks.bID = ?'; }
至此成功获取了写入到数据库的bFilename。
那么一开始说了,其实这个核心还是在于路径穿越导致的任意文件包含,写入到数据库的bFilename也只是个../../../开头的文件路径,那么必然还有一个类似.拼接的操作,带着疑问,又调试了一波。
前面得出是在concrete/src/Area/Area.php:824 加载到了bFilename,接着853$bv->render('view');
跟进161的setupRender:
153通过$this->block->getBlockFilename()返回了$bFilename,这里还是../../的形式,接着往下进入BlockViewTemplate的构造函数:
在computeView:105行断点的地方,最终发现了罪魁祸首。
后面就简单了,在renderViewContents中include:
结语
偶然遇到的rce对学习代码又有了一些新的认识。
参考
https://hackerone.com/reports/1102067