2020-04-27
对不起我来晚了
通达oa2017版本漏洞引发的知识补充
首先写出getshell过程。知错不改,所以才有安全问题。
河北张家口学院使用通达oa2017版本,这个系统存在严重可getshell的漏洞。通过伪造管理员cookie和文件上传可getshell获取大量敏感信息甚至渗透至全部内网。
首先通过伪造cookie实现管理员登录。chrome的edit this cookie插件即可。
找到了上传点 POST是为了将数据传送到服务器段,GET是为了从服务器段取得数据。POST的信息作为http请求的内容,而GET是在Http头部传输的,并且我们传输的文件数据量较大,所以不能用GET请求。更改GET为POST
修改请求,上传文件。 记得修改content-type,这是一个常见的 POST 数据提交的方式。我们使用表单上传文件时,必须使form表单的 enctype属性值等于 multipart/form-data,它会将表单的数据处理为一条消息。另外,Content-disposition,用来说明字段的一些信息,他们以标签为单元,分隔符(boundary)分开。
生成了一个 boundary 用于分割不同的字段,为了避免与正文内容重复,boundary 很长很复杂。然后 Content-Type 里指明了数据是以 mutipart/form-data 来编码,本次请求的 boundary 是什么内容。消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以 --boundary 开始,紧接着内容描述信息,然后是回车,最后是字段具体内容(文本或二进制)。 详细后面会说。
另外,这里不允许上传php类型文件,双写大小写都不可以。但是apache有个解析漏洞,就是把.php.后缀的文件也解析成php类型,所以在文件名后加点即可绕过。
后面2004/1837231923.2.php就是获得的文件存储路径。而我们知道存储路径的前半部分。 试着访问,页面没有报错。 冰蝎连接,成功。
这个漏洞主要是
upload.php - <?php
- set_time_limit(0);
- $P = $_POST['P'];
- if (isset($P) || $P != '') {
- ob_start();
- include_once 'inc/session.php';
- session_id($P);
- session_start();
- session_write_close();
- } else {
- include_once './auth.php';
- }
- include_once 'inc/utility_file.php';
- include_once 'inc/utility_msg.php';
- include_once 'mobile/inc/funcs.php';
- ob_end_clean();
- $TYPE = $_POST['TYPE'];
- $DEST_UID = $_POST['DEST_UID'];
- $dataBack = array();
- if ($DEST_UID != '' && !td_verify_ids($ids)) {
- $dataBack = array('status' => 0, 'content' => '-ERR ' . _('接收方ID无效'));
- echo json_encode(data2utf8($dataBack));
- exit;
- }
- if (strpos($DEST_UID, ',') !== false) {
- } else {
- $DEST_UID = intval($DEST_UID);
- }
- if ($DEST_UID == 0) {
- if ($UPLOAD_MODE != 2) {
- $dataBack = array('status' => 0, 'content' => '-ERR ' . _('接收方ID无效'));
- echo json_encode(data2utf8($dataBack));
- exit;
- }
- }
- $MODULE = 'im';
- if (1 <= count($_FILES)) {
- if ($UPLOAD_MODE == '1') {
- if (strlen(urldecode($_FILES['ATTACHMENT']['name'])) != strlen($_FILES['ATTACHMENT']['name'])) {
- $_FILES['ATTACHMENT']['name'] = urldecode($_FILES['ATTACHMENT']['name']);
- }
- }
- $ATTACHMENTS = upload('ATTACHMENT', $MODULE, false);
- if (!is_array($ATTACHMENTS)) {
- $dataBack = array('status' => 0, 'content' => '-ERR ' . $ATTACHMENTS);
- echo json_encode(data2utf8($dataBack));
- exit;
- }
- ob_end_clean();
- $ATTACHMENT_ID = substr($ATTACHMENTS['ID'], 0, -1);
- $ATTACHMENT_NAME = substr($ATTACHMENTS['NAME'], 0, -1);
- if ($TYPE == 'mobile') {
- $ATTACHMENT_NAME = td_iconv(urldecode($ATTACHMENT_NAME), 'utf-8', MYOA_CHARSET);
- }
- } else {
- $dataBack = array('status' => 0, 'content' => '-ERR ' . _('无文件上传'));
- echo json_encode(data2utf8($dataBack));
- exit;
- }
- $FILE_SIZE = attach_size($ATTACHMENT_ID, $ATTACHMENT_NAME, $MODULE);
- if (!$FILE_SIZE) {
- $dataBack = array('status' => 0, 'content' => '-ERR ' . _('文件上传失败'));
- echo json_encode(data2utf8($dataBack));
- exit;
- }
- if ($UPLOAD_MODE == '1') {
- if (is_thumbable($ATTACHMENT_NAME)) {
- $FILE_PATH = attach_real_path($ATTACHMENT_ID, $ATTACHMENT_NAME, $MODULE);
- $THUMB_FILE_PATH = substr($FILE_PATH, 0, strlen($FILE_PATH) - strlen($ATTACHMENT_NAME)) . 'thumb_' . $ATTACHMENT_NAME;
- CreateThumb($FILE_PATH, 320, 240, $THUMB_FILE_PATH);
- }
- $P_VER = is_numeric($P_VER) ? intval($P_VER) : 0;
- $MSG_CATE = $_POST['MSG_CATE'];
- if ($MSG_CATE == 'file') {
- $CONTENT = '[fm]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $FILE_SIZE . '[/fm]';
- } else {
- if ($MSG_CATE == 'image') {
- $CONTENT = '[im]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $FILE_SIZE . '[/im]';
- } else {
- $DURATION = intval($DURATION);
- $CONTENT = '[vm]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $DURATION . '[/vm]';
- }
- }
- $AID = 0;
- $POS = strpos($ATTACHMENT_ID, '@');
- if ($POS !== false) {
- $AID = intval(substr($ATTACHMENT_ID, 0, $POS));
- }
- $query = 'INSERT INTO im_offline_file (TIME,SRC_UID,DEST_UID,FILE_NAME,FILE_SIZE,FLAG,AID) values (\'' . date('Y-m-d H:i:s') . '\',\'' . $_SESSION['LOGIN_UID'] . '\',\'' . $DEST_UID . '\',\'*' . $ATTACHMENT_ID . '.' . $ATTACHMENT_NAME . '\',\'' . $FILE_SIZE . '\',\'0\',\'' . $AID . '\')';
- $cursor = exequery(TD::conn(), $query);
- $FILE_ID = mysql_insert_id();
- if ($cursor === false) {
- $dataBack = array('status' => 0, 'content' => '-ERR ' . _('数据库操作失败'));
- echo json_encode(data2utf8($dataBack));
- exit;
- }
- $dataBack = array('status' => 1, 'content' => $CONTENT, 'file_id' => $FILE_ID);
- echo json_encode(data2utf8($dataBack));
- exit;
- } else {
- if ($UPLOAD_MODE == '2') {
- $DURATION = intval($_POST['DURATION']);
- $CONTENT = '[vm]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $DURATION . '[/vm]';
- $query = 'INSERT INTO WEIXUN_SHARE (UID, CONTENT, ADDTIME) VALUES (\'' . $_SESSION['LOGIN_UID'] . '\', \'' . $CONTENT . '\', \'' . time() . '\')';
- $cursor = exequery(TD::conn(), $query);
- echo '+OK ' . $CONTENT;
- } else {
- if ($UPLOAD_MODE == '3') {
- if (is_thumbable($ATTACHMENT_NAME)) {
- $FILE_PATH = attach_real_path($ATTACHMENT_ID, $ATTACHMENT_NAME, $MODULE);
- $THUMB_FILE_PATH = substr($FILE_PATH, 0, strlen($FILE_PATH) - strlen($ATTACHMENT_NAME)) . 'thumb_' . $ATTACHMENT_NAME;
- CreateThumb($FILE_PATH, 320, 240, $THUMB_FILE_PATH);
- }
- echo '+OK ' . $ATTACHMENT_ID;
- } else {
- $CONTENT = '[fm]' . $ATTACHMENT_ID . '|' . $ATTACHMENT_NAME . '|' . $FILE_SIZE . '[/fm]';
- $msg_id = send_msg($_SESSION['LOGIN_UID'], $DEST_UID, 1, $CONTENT, '', 2);
- $query = 'insert into IM_OFFLINE_FILE (TIME,SRC_UID,DEST_UID,FILE_NAME,FILE_SIZE,FLAG) values (\'' . date('Y-m-d H:i:s') . '\',\'' . $_SESSION['LOGIN_UID'] . '\',\'' . $DEST_UID . '\',\'*' . $ATTACHMENT_ID . '.' . $ATTACHMENT_NAME . '\',\'' . $FILE_SIZE . '\',\'0\')';
- $cursor = exequery(TD::conn(), $query);
- $FILE_ID = mysql_insert_id();
- if ($cursor === false) {
- echo '-ERR ' . _('数据库操作失败');
- exit;
- }
- if ($FILE_ID == 0) {
- echo '-ERR ' . _('数据库操作失败2');
- exit;
- }
- echo '+OK ,' . $FILE_ID . ',' . $msg_id;
- exit;
- }
- }
- }
复制代码
从图片里可以看出,这段代码的问题是 P需要有值且不为空,auth.php 进行登录验证。所以poc中,需要有 Content-Disposition 的 name='p' 字段,来存放管理员cookie。
另外,分析可知还需要一个DEST_UID的值,这个值不可以为0或者空。
还需要一个UPLOAD_MODE值,为2;
最后就是文件内容和文件类型了。这时会对"$_FILES['ATTACHMENT']['name'])"进行一次url解码,之后判断解码前后文件名长度是否有变化,如果有变化,则将url解码后的文件名作为最后的文件名。 - function upload($PREFIX = 'ATTACHMENT', $MODULE = '', $OUTPUT = true)
- {
- if (strstr($MODULE, '/') || strstr($MODULE, '\\')) {
- if (!$OUTPUT) {
- return _('参数含有非法字符。');
- }
- Message(_('错误'), _('参数含有非法字符。'));
- exit;
- }
- $ATTACHMENTS = array('ID' => '', 'NAME' => '');
- reset($_FILES);
- foreach ($_FILES as $KEY => $ATTACHMENT) {
- if ($ATTACHMENT['error'] == 4 || $KEY != $PREFIX && substr($KEY, 0, strlen($PREFIX) + 1) != $PREFIX . '_') {
- continue;
- }
- $data_charset = isset($_GET['data_charset']) ? $_GET['data_charset'] : (isset($_POST['data_charset']) ? $_POST['data_charset'] : '');
- $ATTACH_NAME = $data_charset != '' ? td_iconv($ATTACHMENT['name'], $data_charset, MYOA_CHARSET) : $ATTACHMENT['name'];
- $ATTACH_SIZE = $ATTACHMENT['size'];
- $ATTACH_ERROR = $ATTACHMENT['error'];
- $ATTACH_FILE = $ATTACHMENT['tmp_name'];
- $ERROR_DESC = '';
- if ($ATTACH_ERROR == UPLOAD_ERR_OK) {
- if (!is_uploadable($ATTACH_NAME)) {
- $ERROR_DESC = sprintf(_('禁止上传后缀名为[%s]的文件'), substr($ATTACH_NAME, strrpos($ATTACH_NAME, '.') + 1));
- }
- $encode = mb_detect_encoding($ATTACH_NAME, array('ASCII', 'UTF-8', 'GB2312', 'GBK', 'BIG5'));
- if ($encode != 'UTF-8') {
- $ATTACH_NAME_UTF8 = mb_convert_encoding($ATTACH_NAME, 'utf-8', MYOA_CHARSET);
- } else {
- $ATTACH_NAME_UTF8 = $ATTACH_NAME;
- }
- if (preg_match('/[\\\':<>?]|\\/|\\\\|"|\\|/u', $ATTACH_NAME_UTF8)) {
- $ERROR_DESC = sprintf(_('文件名[%s]包含[/\\\'":*?<>|]等非法字符'), $ATTACH_NAME);
- }
- if ($ATTACH_SIZE == 0) {
- $ERROR_DESC = sprintf(_('文件[%s]大小为0字节'), $ATTACH_NAME);
- }
- if ($ERROR_DESC == '') {
- $ATTACH_NAME = str_replace('\'', '', $ATTACH_NAME);
- $ATTACH_ID = add_attach($ATTACH_FILE, $ATTACH_NAME, $MODULE);
- if ($ATTACH_ID === false) {
- $ERROR_DESC = sprintf(_('文件[%s]上传失败'), $ATTACH_NAME);
- } else {
- $ATTACHMENTS['ID'] .= $ATTACH_ID . ',';
- $ATTACHMENTS['NAME'] .= $ATTACH_NAME . '*';
- }
- }
- @unlink($ATTACH_FILE);
- } else {
- if ($ATTACH_ERROR == UPLOAD_ERR_INI_SIZE) {
- $ERROR_DESC = sprintf(_('文件[%s]的大小超过了系统限制(%s)'), $ATTACH_NAME, ini_get('upload_max_filesize'));
- } else {
- if ($ATTACH_ERROR == UPLOAD_ERR_FORM_SIZE) {
- $ERROR_DESC = sprintf(_('文件[%s]的大小超过了表单限制'), $ATTACH_NAME);
- } else {
- if ($ATTACH_ERROR == UPLOAD_ERR_PARTIAL) {
- $ERROR_DESC = sprintf(_('文件[%s]上传不完整'), $ATTACH_NAME);
- } else {
- if ($ATTACH_ERROR == UPLOAD_ERR_NO_TMP_DIR) {
- $ERROR_DESC = sprintf(_('文件[%s]上传失败:找不到临时文件夹'), $ATTACH_NAME);
- } else {
- if ($ATTACH_ERROR == UPLOAD_ERR_CANT_WRITE) {
- $ERROR_DESC = sprintf(_('文件[%s]写入失败'), $ATTACH_NAME);
- } else {
- $ERROR_DESC = sprintf(_('未知错误[代码:%s]'), $ATTACH_ERROR);
- }
- }
- }
- }
- }
- }
- if ($ERROR_DESC != '') {
- if (!$OUTPUT) {
- delete_attach($ATTACHMENTS['ID'], $ATTACHMENTS['NAME'], $MODULE);
- return $ERROR_DESC;
- } else {
- Message(_('错误'), $ERROR_DESC);
- }
- }
- }
- return $ATTACHMENTS;
- }
复制代码
当文件名中不存在.时就直接利用现有的文件名,当存在.的时候往后匹配三位,另外判断文件名是否为php,不可以上传php类型文件。
所以两种思路:
1.上传.php.后缀文件
2.上传图片马配合文件包含漏洞(不在今天讨论范围内)
POST请求的消息主体放在entity body中,服务端根据请求头中的Content-Type字段来获取消息主体的编码方式,进而进行解析数据。 上面说的content-type的取值,有4种类型: 1.application/x-www-form-urlencoded 最常见的 POST 提交数据的方式,原生Form表单,如果不设置 enctype 属性,默认为application/x-www-form-urlencoded 方式提交数据。
首先,Content-Type被指定为 application/x-www-form-urlencoded;其次,提交的表单数据会转换为键值对并按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL 转码。大部分服务端语言都对这种方式有很好的支持。 另外,如利用AJAX 提交数据时,也可使用这种方式。例如 jQuery,Content-Type 默认值都是”application/x-www-form-urlencoded;charset=utf-8”。
2.multipart/form-data 上面写了
3.application/json 作为响应头比较常见。实际上,现在越来越多的人把它作为请求头,用来告诉服务端消息主体是序列化后的 JSON 字符串,其中一个好处就是JSON 格式支持比键值对复杂得多的结构化数据。由于 JSON 规范的流行,除了低版本 IE 之外的各大浏览器都原生支持JSON.stringify,服务端语言也都有处理 JSON 的函数,使用起来没有困难。 Google 的 AngularJS 中的 Ajax 功能,默认就是提交 JSON 字符串。
4.text/xml XML的作用不言而喻,用于传输和存储数据,它非常适合万维网传输,提供统一的方法来描述和交换独立于应用程序或供应商的结构化数据,在JSON出现之前是业界一大标准(当然现在也是),相比JSON的优缺点大家有兴趣可以上网search。因此,在POST提交数据时,xml类型也是不可缺少的一种,虽然一般场景上使用JSON可能更轻巧、灵活。
5.binary (application/octet-stream)
在Chrome浏览器的Postman工具中,还可以看到”binary“这一类型,指的就是一些二进制文件类型。如application/pdf,指定了特定二进制文件的MIME类型。就像对于text文件类型若没有特定的子类型(subtype),就使用 text/plain。类似的,二进制文件没有特定或已知的 subtype,即使用 application/octet-stream,这是应用程序文件的默认值,一般很少直接使用 。
对于application/octet-stream,只能提交二进制,而且只能提交一个二进制,如果提交文件的话,只能提交一个文件,后台接收参数只能有一个,而且只能是流(或者字节数组)。
很多web服务器使用默认的 application/octet-stream 来发送未知类型。出于一些安全原因,对于这些资源浏览器不允许设置一些自定义默认操作,导致用户必须存储到本地以使用。一般来说,设置正确的MIME类型很重要。
|