沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

代码审计入门之PopojiCMS漏洞复现

0x00 前言


本文寻找了一个PHP的CMS进行漏洞复现。

0x01 获取复现目标


寻找目标

有的同学可能不知道怎么才能找到一个CMS进行漏洞复现,这里就稍微说一下。

  • 国家信息安全漏洞库 (cnnvd.org.cn)中的“漏洞信息”处可以看到最新和历史漏洞

  • 在其右边,贴心的为我们准备了一个搜索框。

    image-20220301192401940
  • “漏洞名称”输入“CMS”;编号省略;发布时间,我建议是将选择范围限定在近几年,这样可以保证我们待会还可以获得CMS的源码进行环境搭建。

  • 物色好一个顺眼的CMS(保证可以得到对应漏洞版本源码)后,就可以重新搜索,只搜索此CMS的历史漏洞。

这里我最后选择了PopojiCMS进行漏洞的复现(在这之前,还尝试了另一个,可惜最后环境搭建失败了)。

源码获取

一般点开漏洞详情,都会有相关的链接,只要顺着这些链接,就大概率能找到项目地址了。一般大家申请CVE时,都会直接在Github项目发起一个issue,所以很多时候,点击漏洞详情里的参考网址,就能找到对应的开源项目。如果找不到,还可以试着谷歌搜索该CMS,到官网上下载源码。

环境搭建

如果像我一样懒的话,可以直接使用phpstudy进行运行环境的搭建。搭建过程中对于PHP拓展还有数据库的什么要求,就要自己看项目的Readme或者安装文档了。phpstudy的好处就是,他是集成式的,可以很方便的切换PHP版本、安装PHP插件、创建数据库。

搭建好运行环境后,还可以搭建一下调试环境。由于不是本篇的重点,所以建议自己谷歌搜索关键词:vscode的PHP调试环境搭建、PHPStorm的调试环境搭建。

0x02 漏洞复现


PopojiCMS 重定向漏洞

CNNVD-201911-414

寻找漏洞点

一开始我是自己慢慢看代码进行漏洞寻找的,结果并没有找到……还是太菜了。好在这个漏洞,师傅为了交CVE,在Github提交了issue,并且给出了详情,从CNNVD给出的参考网址就能跳转过去。看完之后,怀疑自己智商了🤣。漏洞就在项目根目录的index.php里,是最先能被看到的文件……(悲)

所以,我最后还是放弃了手动寻找漏洞的方法,把项目拖进了Seay里。这个工具老是老,但是确实有作用:

image-20220301195408897

与issue对比一下,发现漏洞确实是在这处。所以,我觉得对于初学者来说,如果实在难以下手,先使用自动审计系统扫一遍,然后自己手动查验,确实是一种更好的入门方式,毕竟代码审计功力也是要有积累的。

相关代码如下:

$core = new PoCore();

if (isset($_POST['lang'])) {
    setcookie('po_lang_front', $core->postring->valid($_POST['lang'], 'xss'), 1719241200, '/');
    $current_lang = $core->podb->from('language')
        ->where('code', $core->postring->valid($_POST['lang'], 'xss'))
        ->limit(1)
        ->fetch();
    define('WEB_LANG_ID', $current_lang['id_language']);
    define('WEB_LANG', $current_lang['code']);
    header('location:'.$_POST['refer']);//漏洞点
} elseif (isset($_COOKIE['po_lang_front'])) {
    //....
}

index.php中如果post了lang参数,就会重定向到refer指定的地址,并且这两个参数都是可控和未过滤的,所以就造成了可以重定向到任意一个网站。

复现

使用Burp抓包,把请求方式改为POST,并记得加上Content-Type: application/x-www-form-urlencoded,然后构造lang和refer参数,如下图:

image-20220301201522485

复现成功,成功跳转到了我们本地搭建的另一个网站。

思考

这种任意跳转的漏洞有什么危害呢?感觉危害好像并不大。如果是GET型的任意跳转,我们可以想到的利用方式是,将URL发给别人,点击后跳转到一个钓鱼网站,这在黑产中还挺常用的;但是这里是POST型的任意跳转,我感觉危害比GET型又小了很多。

思考了一下之后,感觉可以和CSRF结合一下,因为这里好像也算一个CSRF,下面尝试一下,用Burp生成CSRF POC:

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body>
  <script>history.pushState('', '', '/')</script>
    <form action="http://popp/" method="POST">
      <input type="hidden" name="lang" value="cn" />
      <input type="hidden" name="refer" value="http&#58;&#47;&#47;test&#46;hah&#47;" />
      <input type="hidden" name="" value="" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      document.forms[0].submit();
    </script>
  </body>
</html>

放到另一个网站里,确实成功跳转了。不过感觉还是没什么危害……下次去乌云看看大师傅们的思路。

PopojiCMS 跨站脚本漏洞

寻找漏洞点

这种漏洞,我个人是建议先进行黑盒测试,搞清楚业务逻辑和流程,先找到可能出现XSS的地方测试一下,然后再找到源码中对应处,白盒查看是否真的存在,或者能否绕过过滤等。

总之,根据描述,先进入后台再说。这里有一个小插曲就是,这个CMS后台登陆处居然没又输入密码处(好像是加载原因)……好在可以抓包传参登录。

打开后台后,有很多可以管理文本编辑的地方,其中一个就是post处,可以添加文章:

image-20220301221109050

骗CVE?

CNNVD-201911-413(CVE-2019-18816)

issue显示,就是在后台Post的编辑处。烦人的是,他这里点击提交又没用……还好我们可以控制台使用document.forms[0].submit(),然后抓包修改数据(发现提交的数据转义了,我们修改回来),最后还是没有测试成功,查看数据库里,还是被转义了:

image-20220302200537259

找到代码处(调试跟进到了po-content\component\post\admin_post.php):

if ($othlang_post > 0) {//如果已经存在,则使用UPdate
    $post_description = array(
        'title' => $this->postring->valid($value['title'], 'xss'),
        'content' => stripslashes(htmlspecialchars($value['content'],ENT_QUOTES))
    );
    $query_post_description = $this->podb->update('post_description')
        ->set($post_description)
        ->where('id_post_description', $this->postring->valid($value['id'], 'sql'));
}

可以看到上面content处,确实是有实体转义函数htmlspecialchars,并且指定了参数ENT_QUOTES(转义单双引号)的,而查看漏洞issue,他好像也并没有做其他操作:Stored XSS on Post feature · Issue #21。是我们的源码已经做了修复吗,我找到admin_post.php的历史commit,发现这里从最早的commit就有转义的,所以我不知道是什么原因,难道这里他成功骗了一个CVE?又或者是我太菜了,不知道绕过?

另一个XSS

CNNVD-202108-2367

在Menu Manager处,可以修改组件的名字,在这里存在一个存储型XSS

image-20220302205932011

成功弹窗,查看源码(po-content\component\menumanager\admin_menumanager.php):

public function savemenu()
{
    if (!$this->auth($_SESSION['leveluser'], 'menumanager', 'create')) {
        echo $this->pohtml->error();
        exit;
    }
    include_once '../'.DIR_CON.'/component/menumanager/modules/menu.php';
    $instance = new Menu;
    $instance->save();
}

实例化一个Menu类,然后调用其save方法,继续跟进:

public function save()
{
    if (isset($_POST['title'])) {
        $data = array(
            MENU_TITLE => trim($_POST['title']),
            MENU_ID => $_POST['menu_id'],
            MENU_URL => $_POST['url'],
            MENU_CLASS => $_POST['class'],
            MENU_ACTIVE => $_POST['active'],
            MENU_TARGET => $_POST['target']
        );
        if (!empty($data[MENU_TITLE])) {
				$query = $this->podb->update(MENU_TABLE)
					->set($data)
					->where(MENU_ID, $data[MENU_ID]);
        }
	//省略了不相关代码
    }
}

没有任何防护。其实这里url处也可以XSS:

image-20220302212414323

经测试,也是可以成功的。如果这里ID用的不是PDO方式的SQL查询,明显也可以堆叠注入,还不太熟悉PDO,大概率这里是不行了。

X不进去,不代表没有漏洞

CNNVD-202108-690

这里的XSS出现在User更新处(Storage XSS in Tambah Pengguna · Issue #24):

image-20220302225410782

这里黑盒测试,使用"><script>alert(1)</script>@test.com"就可以直接闭合input标签测出XSS。但在我白盒审计此处的时候,遇到了一点有趣的事情。首先先看代码:

$this->poval->validation_rules(array(
    'nama_lengkap' => 'required|max_len,255|min_len,3',
    'email' => 'required|valid_email',
    'no_telp' => 'required'
));
$this->poval->filter_rules(array(
    'nama_lengkap' => 'trim|sanitize_string',
    'email' => 'trim|sanitize_email',
    'no_telp' => 'trim'
));
$validated_data = $this->poval->run(array_merge($_POST, $_FILES));

if ($validated_data === false) {
    $this->poflash->error($GLOBALS['_']['user_message_5'], 'admin.php?mod=user&act=edit&id='.$this->postring->valid($_POST['id'], 'xss'));
} else {
    $data = array(
        'nama_lengkap' => $_POST['nama_lengkap'],
        'email' => $_POST['email'],
        'no_telp' => $_POST['no_telp'],
        'bio' => htmlspecialchars($_POST['bio'], ENT_QUOTES),
        'locktype' => $this->postring->valid($_POST['locktype'], 'xss')
    );
}

这里前一段代码会进行一个规则的检查,这也是我们的payload要有@test.com的原因(这里的sanitize,消毒,好像也并没有发挥什么用处……)。不过,重点是$_POST[‘no_telp’],我们的电话号码。从代码上可以看出,只进行了去空和必需的操作与规定,所以实际上,如果从白盒上审计,最先注意到的点,应该是这处。

看起来好像可以随便X?但是当我使用类似的payload:"><script>alert(1)</script>,却发现无论如何,都不能成功插入……调试了半天后,我发现问题是出在SQL的执行上,每次都会执行失败。一番尝试与思考后,我突然想到,SQL执行失败是不是因为表中对数据格式和长度有所要求?使用describe users查看表结构如下:

image-20220302230659523

果然是!!!

image-20220302230808080

很明显是payload的长度过长了,从而导致SQL执行失败。好在这里是input标签的value,我们可以使用" onfocus=alert(1);>来XSS,并且长度正好是20。把上面的payload,填入Phone Number栏,最后果然成功XSS。😋

所以,这个故事告诉我们,有时候X不进去,并不代表没有漏洞,而是存在其他的一些限制,多尝试,多测试,才能确保不略过。

PopojiCMS 跨站请求伪造漏洞

复现失败

CNNVD-201903-028

这个漏洞出在用户的添加上。其实查看源码,就会发现,几乎所有的操作都没有进行CSRF的防御。但是这里为什么复现失败了呢?一开始我也很不理解,后来调试了半天发现问题依然是出在数据库上。

源码如下:

$data = array(
    'id_user' => $last_user['id_user']+1,
    'username' => $_POST['username'],
    'password' => md5($_POST['password']),
    'nama_lengkap' => $_POST['nama_lengkap'],
    'email' => $_POST['email'],
    'no_telp' => $_POST['no_telp'],
    'level' => $_POST['level'],
    'tgl_daftar' => date('Ymd'),
    'id_session' => md5($_POST['password'])
);
$query = $this->podb->insertInto('users')->values($data);
$query->execute();

但是当我们手动构造Insert语句时,却会有如下的错误:

image-20220304231031147

我们少传了bio字段,而bio字段没有默认值,并且他是text类型,意味着他也不能有默认值。或许这个bio字段是后来作者加入的,不过我也没有什么兴趣去看历史记录了。

由于这里大部分(可能是全部)操作都没有进行CSRF的防御,所以不打算复现此CMS别的CSRF漏洞了。

PopojiCMS 路径遍历漏洞

任意文件删除

CNNVD-201811-068

Library下可以进行删除操作:

public function delete()
{
    if (!$this->auth($_SESSION['leveluser'], 'library', 'delete')) {
        echo $this->pohtml->error();
        exit;
    }
    if (!empty($_POST)) {
        if ($this->postring->isImage('../'.DIR_CON.'/uploads/'.$_POST['id'])) {
            if (file_exists('../'.DIR_CON.'/thumbs/'.$_POST['id'])) {
                unlink('../'.DIR_CON.'/thumbs/'.$_POST['id']);
            }
            if (file_exists('../'.DIR_CON.'/uploads/medium/medium_'.$_POST['id'])) {
                unlink('../'.DIR_CON.'/uploads/medium/medium_'.$_POST['id']);
            }
            if (file_exists('../'.DIR_CON.'/uploads/'.$_POST['id'])) {
                unlink('../'.DIR_CON.'/uploads/'.$_POST['id']);
            }
        } else {//不是图片
            if (file_exists('../'.DIR_CON.'/uploads/'.$_POST['id'])) {
                unlink('../'.DIR_CON.'/uploads/'.$_POST['id']);
            }
        }
        $this->poflash->success($GLOBALS['_']['library_message_2'], 'admin.php?mod=library');
    }
}

对$_POST[‘id’]没有作任何过滤,并且在逻辑上,当不是图片时且存在时也会去删除, 所以我们可以利用../进行目录穿越,并删除任意文件。

任意文件读取

CNNVD-202108-2368

Themes中可以进行修改操作,但是对id却只进行XSS的验证:

if (file_exists('../'.DIR_CON.'/themes/'.$this->postring->valid($_GET['folder'], 'xss').'/'.$this->postring->valid($_GET['id'], 'xss'))) {
    $fh = fopen('../'.DIR_CON.'/themes/'.$this->postring->valid($_GET['folder'], 'xss').'/'.$this->postring->valid($_GET['id'], 'xss'), "r") or die("Could not open file!");
    $data = fread($fh, filesize('../'.DIR_CON.'/themes/'.$this->postring->valid($_GET['folder'], 'xss').'/'.$this->postring->valid($_GET['id'], 'xss'))) or die("Could not read file!");
    $data = str_replace("textarea", "textareapopojicms", $data);
    fclose($fh);
}

这里同样可以通过../进行目录穿越,进行任意文件读取。

PopojiCMS 后台GetShell

CNNVD-201811-066

算漏洞吗?

这个漏洞是通过上传含有shell的压缩包,后端会帮我们解压,导致可以上传shell文件。但有的师傅也认为这是后来正常的功能,不能算作漏洞,看个人吧。

if (in_array($exp[1], array('zip'))) {
    move_uploaded_file($_FILES['fupload']['tmp_name'], '../'.DIR_CON.'/uploads/'.$componentName);
    if (file_exists('../'.DIR_CON.'/'.$folderinstall.'/'.strtolower($this->postring->valid($_POST['component'], 'xss')))) {
        unlink('../'.DIR_CON.'/uploads/'.$componentName);
        $this->poflash->error($GLOBALS['_']['component_message_3'], 'admin.php?mod=component');
    } else {
        $archive = new PoPclZip('../'.DIR_CON.'/uploads/'.$componentName);
        if ($archive->extract(PCLZIP_OPT_PATH, '../'.DIR_CON.'/'.$folderinstall.'/'.strtolower($this->postring->valid($_POST['component'], 'xss'))) == 0) {
            unlink('../'.DIR_CON.'/uploads/'.$componentName);
            $this->poflash->error($GLOBALS['_']['component_message_3'], 'admin.php?mod=component');
        }

会把zip解压到po-content\component(widget)\xxx目录下

image-20220305001339148