GeekGame3 Writeup
参赛ID:Lysithea 分数:3886 [TOC]
misc+tutorial
一眼盯帧(签到)
用一个支持逐帧查看GIF的图片浏览器(我用的HoneyView),把每帧上的字符逐个抄下来,是一串英文乱码synt{jrypbzrarjcynlref}
。考虑到题 面说答案是有意义的英文(以及签到题的难度),首先尝试的是最常用的凯撒密码(字母移位加密),移动13位即可解出flag{welcomenewplayers}
历年最简单签到题(确信)
小北问答(信息收集)
今年不搞花里胡哨的了,挺好的。
- 北大HPC的非交互式提交任务的方式
访问hpc官网的使用教程,提交作业章节,提到了sbatch运行作业,和salloc交互运行作业。虽然没有明说,但是sbatch应该就是非交互式的。
- Redmi K60 Ultra开源的Linux内核版本号
卡最久的题。首先在搜索Redmi K60 Ultra open source Linux kernel能搜到小米的github仓库,master分支下能看到各个开源版本的汇总表,最后一行能看到K60 Ultra的分支名称是corot-s-oss。切到这个分支,点开commits,发现只有最新的commit来自小米,前面的都是上游项目的。
Linux内核版本在项目的主Makefile文件开头,几个环境变量的定义中,分别是x,y,z版本号。之所以能确信这是版本号,其实还和下面踩的坑2有关。
# SPDX-License-Identifier: GPL-2.0
VERSION = 5
PATCHLEVEL = 15
SUBLEVEL = 78
EXTRAVERSION =
NAME = Trick or Treat
这题踩了两个坑浪费了相当多时间
- 尝试根据tag名t-alps-release-t0.mp1.tc8sp2-V1.14和来源mtk(mediatek,也是一家手机公司),去寻找他们的开源代码,但是没找到这个tag对应的release(而且说实话就算找到了也大概率不会解决问题,除非他们把kernel version直接写在readme里)
- 如果没注意到只有最新的一个commit来自小米,往前翻到大概9800+个commit,会看到一个地方突然commit格式变得自由散漫了起来,还有个超明显的
Linux 5.15.31
,当时以为这就是红米从Linux Kernel继承的原点。实际上,MTK的Android内核本身也是基于Linux的魔改,也是需要开源的,这个可能是MTK项目组从Linux Kernel继承的commit。
- Apple Watch Series 8 蜂窝 41mm 内部识别号
直接搜这些关键词和 Identifier,在github gist有人统计了这些数据,可以直接查到。Cellular是蜂窝的意思,这里应该是指类似4G流量这样的蜂窝网络技术。
- 被平台ban掉的用户昵称可用字符总数
正确解法是去xmcp老师开源的guidingstar比赛平台的github仓库里找源码。注意这个平台分为前端后端,我们找的显然是后端的python代码。看看commit,按题面要求找到10.1的commit,一番寻找找到平台用户信息设置相关的代码,即src/store/user_profile_store.py
很显然平台代码的大部分逻辑可以直接搬下来在本地运行,只需要把一些不必要的依赖去掉就行了(不过有个unicategories的库感觉可能会影响结果,我自己安装了)。然后就如提示所说,不同版本运行结果是不一样的。在平台Readme里提到Python的最低版本是3.8,于是我就在自己的anaconda上用3.8-3.12所有大版本都跑了一遍,结果是3.12是4636个,3.11是4587个,3.10/3.9是4472个,3.8是4445个。我是从新版本到旧版本开始试的,很不幸的生产环境用的是最旧的版本,笑死。
PS:能想到平台开源这点也是因为赛前刷比赛群,xmcp老师发了一个有人扫平台的截图,然后说了一句【平台都开源的,想扫的话这边建议本地慢慢扫呢】,笑死
PPS:其实平台的昵称前端那里还是有BUG。如果名字带【ℒ𝓎𝓈𝒾𝓉𝒽ℯ𝒶】这样的Unicode花体字母,实际上一个字符是占两个字符位置的,但是总字符数显示还是按一个字符算的,这就导致我这个ID只能再额外写4个字符就输不进去了,但是显示字符数还是12/20。应该不是安全问题(……吧?)
- 2011.1年Bilibili游戏区下的子分区
看到题面第一想到的是archive.org,很不幸的是2011.1虽然有一个条目,但是却提示Directory Listing Denied。然后灵感乍现,莫非这个时候bilibili还叫mikufans?于是去中文维基百科上搜索bilibili的历史,意外发现b站在这个时候的域名是bilibili.us,(mikufans是2009年,2011.6改成bilibili.tv,而2014.9才改成.com域名的)
用bilibili.us在archive.org上搜索,你会发现2011年这段时间快照其实非常多,随便点进去一个,找到游戏区链接,最上面就可以看到所有分区了。(我猜很多人的第一反应是:怎么这么少?这真的是所有的了吗?)
- 照片中建筑物的官网域名?
GeekGame第一次出OSINT照片开盒题?拿下来首先看元数据,时间戳很新,所以不排除最近修改的可能性,EXIF等等信息也被抹掉了。然后尝试直接用Google Lens截取图片背景部分(没被旗子遮挡的部分)进行搜索,直接搜出了卢森堡音乐厅,对应官网是philharmonie.lu。这其实就是正解,这样特征明显的大型建筑物不太可能会有第二个。
一开始我还不太信,因为这个前景的赞助商怎么看都是科技公司,应该和音乐厅扯不上关系?所以尝试通过赞助商名字搜索对应的会展名称,没得到特别可靠的结果。最后还是关注到背景小货车上的文字,尽管被树遮挡了一部分,剩下的好像是sound.lu字样,恰好卢森堡国的顶级域名也是lu,这就交叉验证了,这个建筑物确实就是卢森堡音乐厅。
Z公司的服务器(zmodem, jpeg格式)
(flag2解了一半)
这题给了一个流量包和一个终端。流量包里只有一个会话,是192.168.23.179和192.168.16.1双端通信,全部是TCP协议的流量。比较明显的特征是,有个前者(服务端)发送给后者(客户端)的带flag.jpg字符串的包,和一个有大量数据的包,里面能找到jpeg文件头ffd8和文件尾ffd9,所以这应该是客户端向服务端发起的一次文件请求下载过程。
这时候再看终端,就会发现远端返回的报文和流量包里服务端的一模一样。我们可以把流量包里发送的流量重放一次,这次得到的不再是图片文件,而是明文的flag1,镶嵌在前后位置的协议头尾之间。
flag1明文传输是一个很重要的信息,这意味着我们可以把流量包里ffd8到ffd9之间的数据复制出来(更好的是这两个标志都只出现过一次),这一定包含了完整的flag.jpg的信息。但是dump出来之后,会发现图片无法打开,用010 Editor查看会发现除了ffd8以外其他所有section全部对不上,文件中包含了大量的18字节和4X字节,这明显是不正常的。
00000000 ff d8 ff e0 18 40 18 50 4a 46 49 46 18 40 18 41 |.....@.PJFIF.@.A|
00000010 18 41 18 41 18 40 60 18 40 60 18 40 18 40 ff db |.A.A.@`.@`.@.@..|
00000020 18 40 43 18 40 18 42 18 41 18 41 18 42 18 41 18 |.@C.@.B.A.A.B.A.|
00000030 41 18 42 18 42 18 42 18 42 18 42 18 42 18 42 18 |A.B.B.B.B.B.B.B.|
00000040 42 18 43 18 45 18 43 18 43 18 43 18 43 18 43 18 |B.C.E.C.C.C.C.C.|
文件第一个非正常段是ffe0,即APP0,这个段包含了一个JFIF字符串,但是会发现它的位置似乎不对,如果把所有18都去掉就对了,此外JFIF应该以00
结尾,但是这里确实18 40
,到这个时候,差不多也该猜到18其实是类似转义符的作用,不会占有位置,而是会把后面的字符减去0x40。类似的过程,我们会发现0x18会出现在0x40-0x5f, 0xc0-0xdf, 0x69这些字符的前面。处理这些转义之后,我得到的是下面这张图。
虽然能打开但就像被磨坏了的CD一样。jpeg之所以会出现这种情况,是因为其并非按像素存储,而是采取霍夫曼编码,每个数据字节影响的不只是一个像素,所以文件中是存在一些坏数据需要处理的。这可能说明我的程序还有BUG/我对协议的理解有误。(如果其他人做出来的也是这种坏图,我在想有没有拼图大师能把flag给拼出来)
另外,我在一阶段就查到了这种协议被称为ZMODEM,毕竟0x18转义是个鲜明的特征,很容易搜到。虽然协议上说的异或0x40,我个人认为和我这里的处理是等价的,因为0x18事实上没有出现在任何bit6=0的字符前面。写这个writeup的时候我在想,是不是可以用pwntools建立一个伪造的服务端,用一个真正的ZMODEM客户端连接它请求flag.jpg文件,通过重放服务端流量的方式,在客户端直接得到原始文件?似乎比手搓协议要更不容易出BUG。
PS:我一开始对这个题面的语文理解出了偏差,我以为flag1是黑客的方法,所以我是在用pwn的视角理解第一问的,看到周期性报文和返回那么多数据还以为是某种heartbleed。
猫咪状态监视器(shell源码阅读,python格式化字符串)
打开python服务端脚本,很容易发现STATUS的输入格式化字符串没有做必要的检查:
cmd = "/usr/sbin/service {} status".format(service_name)
尽管这个题是在shell=False环境下执行的,后面这些只会作为service的参数,但输入空格仍然可以同时控制多个参数位置。
接下来从Python角度没思路了,去看看service文件,这实际上是一段shell脚本。看manual我们知道,它实际上是用来执行/etc/init.d
下方脚本的一段语法糖。我们比较关心参数处理部分,几个要点:
while [ $# -gt 0 ]; do
case "${1}" in
...
*)
...
elif [ -z "${SERVICE}" ]; then
SERVICE="${1}"
elif [ -z "${ACTION}" ]; then
ACTION="${1}"
else
OPTIONS="${OPTIONS} ${1}"
fi
shift
;;
esac
done
这里shift指令是抛弃第一个参数,把后面参数往前挪。这里的逻辑就是,把第一个参数作为SERVICE,第二个作为ACTION,其他的全部作为OPTIONS放在后面,因此通过插入空格我们可以控制ACTION参数。
run_via_sysvinit() {
# Otherwise, use the traditional sysvinit
if [ -x "${SERVICEDIR}/${SERVICE}" ]; then
exec env -i ... "$SERVICEDIR/$SERVICE" ${ACTION} ${OPTIONS}
else
echo "${SERVICE}: unrecognized service" >&2
exit 1
fi
}
case "${ACTION}" in
...
*)
# We try to run non-standard actions by running
# the init script directly.
run_via_sysvinit
;;
esac
这就发现,当ACTION并非预定义的几个标准选项时,service会认为这是自定义的action而运行/etc/init.d/{SERVICE} {ACTIONS} {OPTIONS}
,而这里对可执行文件路径是直接的简单拼接,因此可以直接用../../../
绕过。最终payload为../../../../bin/cat flag.txt