入手的第一道CTF题目:护网杯的一道二阶注入;让我挖到了ZZCMS的一个二阶注入。
而这第二道CTF题目,让我懂得了最短字符写SHELL。
简易版v01
访问题目,提示give me ip。于是加ip参数给它。
可以看到成功执行了ping命令
考虑拼接其他命令,直接来枚举
可以看到最后一条使用%0a来分割命令,成功了
然后通过cat命令读取该文件即可获得flag
源码
<?php
echo "give me ip";
$flag = "flag{exec1111111exec}";
$t = $_REQUEST['ip'];
$t = trim($t);
$sub = array(
'&' => '',
';' => '',
'|' => '',
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);
$t = str_replace(array_keys($sub), $sub, $t);
if (stristr(php_uname('s'), 'Windows NT')) {
$c = shell_exec('ping ' . $t);
} else {
$c = shell_exec('ping -c 1 ' . $t);
}
echo "<pre>{$c}</pre>";
?>
接收到ip参数后,直接对该参数进行了黑名单校验:
将敏感字符全部替换为空。
此时由于校验不严,导致我们可以通过%0a来绕过校验,成功执行其他命令。
知识点补充
分号(;)
使用分号来分割命令时,相当于将命令分割在了不同的行;无论前一行的命令成功还是失败,都不影响下一条命令的执行。
&& 符号
只有前面的命令执行成功了,那么才会执行&&后面的命令。
& 符号
& 表示任务在后台执行
|| 符号
只有前面命令执行失败了,才会执行后面的命令
| 符号
| 表示管道,上一条命令的输出,作为下一条命令的输入
都会执行,但是输出时只会输出最右边的。
%0a
在传入服务端命令执行函数时,可以使用%0a来分割命令。 效果与分号相同。相当于一行一个命令。
简易版v02
访问题目,页面依旧提示 give me ip。
但是这次只接受POST数据。
当输入一个命令时,发现这次没有过滤连接符,但是却限制了字符长度
测试一下是限制了多少字符
经测试总长度限制为21个字符,分号后面只能输入13个字符
而 cat exec02.php
命令正好14个字符
两个思路解答题目:
利用通配符
cat exec*
那么便会输出当前目录下所有以exec开头的文件的内容
最短模式:cat *
写 shell
利用 echo 语句多次写入
方法一: 直接写shell
假设一次写入一个字符,那么也需要额外写16个字符。
echo -n w>>t.php
方法二:利用sh/bash命令来结合写入shell
思路是:先利用echo语句写入shell命令到文件,然后再利用sh命令执行该命令
使用这个方法的原因是,linux操作系统根据文件特征而不会根据后缀来识别文件类型。
sh命令默认一行是一个命令
但是echo 命令在输出时默认会在后面添加换行符,可以使用
-n
选项来禁止输出换行符。- 使用echo -n 将命令写入一行
- 使用反斜杠让shell识别为一行命令
第二个方法比第一个方法还要少两个字符
看一下写的脚本:
这里有三个细节:
- echo后面使用了双引号包裹
- data变量value前面添加了一个r
- Payload里面的$符号,没有在前面添加反斜杠进行转义
第一个细节,shell脚本中,在读到单引号时,后面的字符全部认为是普通字符,因此反斜杠失去了连接命令的功能,也就保持了原有的换行符
第二个细节,python脚本中,会将反斜杠识别为转义字符。可以使用多加一个反斜杠的方式来标志是普通字符;也可以是在字符串前面添加r字符,表示是原始字符串,在原始字符串中所有的字符均视为普通字符。
第三个细节,我们都知道,$符号在shell内是特殊符号,$符号后面跟字符串时,会认为是一个变量。但是如果只是一个单独的$符号,那么则认为是一个普通字符。
因此一般使用命令在linux服务器上写shell时,大多在$符号前面添加反斜杠。但是此处之所以不加,是因为在读到$符号后,后面只有一个反斜杠,而反斜杠被认为了命令拼接符,所以最终$符号被原样输出
- 使用echo -n 将命令写入一行
源码
<?php
$flag = "flag{flag2exec2flag}";
$ipipip = isset($_POST['ip']) ? $_POST['ip'] : die("give me ip please"
;
if (!preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/i', $ipipip)) {
die("ip format error!"
;
}
echo strlen($ipipip);
if (strlen($ipipip) < 7 || strlen($ipipip) > 21) {
die("ip length error!"
;
}
if (stristr(php_uname('s'), 'Windows NT')) {
$cmd = shell_exec('ping ' . $ipipip);
} else {
$cmd = shell_exec('ping -c 1 ' . $ipipip);
}
echo "<pre>{$cmd}</pre>";
进阶版
这次可以看到竟然限制为了7个字符
按照之前题目学习的姿势,直接 cat *
,GET到flag。
后来分享给我题目的人说:这题要求是GETSHELL。因此有了下面的研究学习。
知识点一:
whoami>t
这条命令大家都知道,是将whoami的输出重定向到文件twhoami>t\\
这条命令则是将输出重定向到文件t\
,因为反斜杠是特殊字符,所以需要再加一个反斜杠进行转义>t\\
而这条命令依旧是将输出重定向到文件t\
,但是由于没有输出,所以文件t\
内是空知识点二:
关于ls命令-t
选项 :按照从新到旧的方式排序文件-r
选项 :反向排序
那么便可以利用这样极短的方式进行GETSHELL。
但是由于 ls -tr>g
命令有8个字符,超了,所以我们只是抛弃-r
选项, 逆向输入,顺序输出。
只是用ls -t
命令又会暴露新的问题。
如图可以看到,使用 ls -t>g
命令时,会先创建文件g
` , 然后再执行ls -t
命令
而sh/bash命令,在遇到错误时便会终止程序,因此会以失败告终。
因此最后的这个文件名也需要特定
直接上脚本
最终以失败告终,经过反复观察,得出了结论;
这一句话这么多英文字母p
,而生成文件名时只会生成一个,所以肯定失败
所以这条路没法走了,使用生成文件名的方式写shell时,要保证文件名不重复才行
解决方式:
- 尽可能的拼接出完全不同的文件名
- 使用wget去下载文件
第一种方式:
- 细节一:是为了保证最后面的文件名没有反斜杠,否则使用
sh
命令执行时,会连接后面的exec3.php
字符 - 细节二:是为了保证无重复的文件名,需要着重注意的是空格、问号、引号等特殊符号都需要在前面添加转义符号
所以最终的Payload就成了这么乱糟糟的样子了
第二种方式:
这样看的话,便会更加容易分辨一些
需要注意的是index.html里面的Payload,$符号前面需要加反斜杠
也可以不使用index.html文件,可以指定为任意文件,只是在构造文件名的时候着重留意不要重复、特殊字符记得转义、不要超过字符限制就好
源码
<?php
error_reporting(0);
$flag="flag{8bytes-get-flag}";
if(strlen($_GET[1])<
{
echo shell_exec($_GET[1]);
}
?>
$flag="flag{xxx}";
if(strlen($_GET[1])<
{
echo shell_exec($_GET[1]);
}
临别
感谢maplege分享的题目。
感谢团队HeatSky师傅、Thinking师傅的对我疑惑的解答。