Hacker101 CTF Android 通關紀錄

Before all

最近剛好推坑學員來打,自己上來補一下以前沒打的安卓題XD
整體而言很舒服,挺適合新手的(含 Writeup 大概一節自習弄完ㄅ)

H1 Thermostat

用 decompiler.com 拆包,在 /sources/com/hacker101/level11/PayloadRequest.java 就可以在請求的 requests headers 中找到兩把 flag 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public PayloadRequest(JSONObject jSONObject, final Response.Listener<String> listener) throws Exception {
super(1, "https://8902e68f31e537242087dcf6b1b9ce7b.ctf.hacker101.com/", new Response.ErrorListener() {
public void onErrorResponse(VolleyError volleyError) {
Response.Listener.this.onResponse("Connection failed");
}
});
this.mListener = listener;
String buildPayload = buildPayload(jSONObject);
this.mParams.put("d", buildPayload);
this.mHeaders = new HashMap<>();
MessageDigest instance = MessageDigest.getInstance("MD5");
instance.update("^FLAG^1cc123c2258b6b617ec31b25d4e9f91ec564809f31dee35e2b3051e1724bc9dc$FLAG$".getBytes());
instance.update(buildPayload.getBytes());
this.mHeaders.put("X-MAC", Base64.encodeToString(instance.digest(), 0));
this.mHeaders.put("X-Flag", "^FLAG^cb1bbd86372b3da20e3d3be856526a5367b0ec75d4f699bc82aff2a6f59c9cfd$FLAG$");
}

Intentional Exercise

/sources/com/hacker101/level13/MainActivity.java,找到這一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView((int) R.layout.activity_main);
WebView webView = (WebView) findViewById(R.id.webview);
webView.setWebViewClient(new WebViewClient());
Uri data = getIntent().getData();
String str = "https://46191d6bd16422b230e5f462400be91e.ctf.hacker101.com/appRoot";
String str2 = BuildConfig.FLAVOR;
if (data != null) {
str2 = data.toString().substring(28);
str = str + str2;
}
if (!str.contains("?")) {
str = str + "?";
}
try {
MessageDigest instance = MessageDigest.getInstance("SHA-256");
instance.update("s00p3rs3cr3tk3y".getBytes(StandardCharsets.UTF_8));
instance.update(str2.getBytes(StandardCharsets.UTF_8));
webView.loadUrl(str + "&hash=" + String.format("%064x", new Object[]{new BigInteger(1, instance.digest())}));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}

在 Android 中,getIntent 的方法會在點下圖標/連結或某些系統變化發生時取值,在這邊感覺是拼接一個 url 所以先假設它想取的是一個連結。
再來注意到 /resources/AndroidManifest.xml 裡的 hardcode 值:

1
2
3
4
5
6
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http" android:host="level13.hacker101.com"/>
</intent-filter>

可以看到 http://level13.hacker101.com 的長度正好為 28,大概就是你了…
最後又因為打開 https://46191d6bd16422b230e5f462400be91e.ctf.hacker101.com/appRoot/ 後獲得的連結

1
2

<h1>Welcome to Level13</h1><a href="appRoot/flagBearer">Flag</a>

嘗試拿 hash 為 SHA-256('s00p3rs3cr3tk3y/flagBearer') 去存取成功拿到 flag ><
image

Final Payload: https://46191d6bd16422b230e5f462400be91e.ctf.hacker101.com/appRoot/flagBearer?&hash=8743a18df6861ced0b7d472b34278dc29abba81b3fa4cf836013426d6256bd5e
P.S. 我一開始沒有 ?& 被卡好久…

Oauthbreaker

Flag 1

首先注意到 /sources/com/hacker101/oauth/WebAppInterface.java
當中定義了一個 getFlagPath 的函數:

1
2
3
4
5
  public String getFlagPath() {
int[] iArr = {174, 95, ......};
...... 略
return str + ".html";
}

把它包進去另一個 Java,然後 println 出來就行 XD

solve script: https://www.programiz.com/online-compiler/8vC5V0QcMaODh

Flag 2

分析 /sources/com/hacker101/oauth/MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    package com.hacker101.oauth;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
String authRedirectUri;
Button button;

/* access modifiers changed from: protected */
@RequiresApi(api = 19)
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView((int) R.layout.activity_main);
this.authRedirectUri = "oauth://final/";
try {
Uri data = getIntent().getData();
if (!(data == null || data.getQueryParameter("redirect_uri") == null)) {
this.authRedirectUri = data.getQueryParameter("redirect_uri");
}
} catch (Exception unused) {
}
this.button = (Button) findViewById(R.id.button);
this.button.setOnClickListener(this);
}

public void onClick(View view) {
if (view.getId() == R.id.button) {
String str = null;
try {
str = "https://022f85b6d943e20227ea7920941a21e0.ctf.hacker101.com/oauth?redirect_url=" + URLEncoder.encode(this.authRedirectUri, StandardCharsets.UTF_8.toString()) + "login&response_type=token&scope=all";
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
Intent intent = new Intent("android.intent.action.VIEW");
intent.setData(Uri.parse(str));
startActivity(intent);
}
}
}

觀察一下,就是一個 Oauth URL 請求器,走的是自己的 oauth:// 協議
一開始根本沒有 Oauth URL 可用,所以會是 oauth://final/
首先請求 https://022f85b6d943e20227ea7920941a21e0.ctf.hacker101.com/oauth?redirect_url=oauth://final/login&response_type=token&scope=all
獲得含 token url oauth://final/login?token=afbe631ca8889b2000be4b6c820b6f09
再次請求,就可以在回傳的 oauth url 中拿到 flag
https://022f85b6d943e20227ea7920941a21e0.ctf.hacker101.com/oauth?redirect_url=oauth://final/login?token=afbe631ca8889b2000be4b6c820b6f09&response_type=token&scope=all

Mobile Webdev

拆開,在 /sources/com/hacker101/webdev/MainActivity.java 拿到幾個 endpoint 和 hmac key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
    package com.hacker101.webdev;

import android.net.http.SslError;
import android.os.Bundle;
import android.view.View;
import android.webkit.SslErrorHandler;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
protected String HmacKey = "8c34bac50d9b096d41cafb53683b315690acf65a11b5f63250c61f7718fa1d1d";
Button editButton;
Boolean editing = false;
Button refreshButton;
WebView webView;

private class SSLTolerantWebViewClient extends WebViewClient {
WebView webView;

SSLTolerantWebViewClient(WebView webView2) {
this.webView = webView2;
}

public boolean shouldOverrideUrlLoading(WebView webView2, String str) {
this.webView.loadUrl(str);
return true;
}

public void onReceivedSslError(WebView webView2, SslErrorHandler sslErrorHandler, SslError sslError) {
super.onReceivedSslError(webView2, sslErrorHandler, sslError);
sslErrorHandler.proceed();
}
}

/* access modifiers changed from: protected */
public String Hmac(byte[] bArr) throws Exception {
throw new Exception("TODO: Implement this and expose to JS");
}

/* access modifiers changed from: protected */
@RequiresApi(api = 19)
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView((int) R.layout.activity_main);
this.editButton = (Button) findViewById(R.id.edit);
this.editButton.setOnClickListener(this);
this.refreshButton = (Button) findViewById(R.id.refresh);
this.refreshButton.setOnClickListener(this);
this.webView = (WebView) findViewById(R.id.webview);
WebView webView2 = this.webView;
webView2.setWebViewClient(new SSLTolerantWebViewClient(webView2));
this.webView.getSettings().setJavaScriptEnabled(true);
this.webView.loadUrl("https://477e4c09ef978f693ce0ea1dac93d31e.ctf.hacker101.com/content/");
}

public void onClick(View view) {
int id = view.getId();
if (id != R.id.edit) {
if (id == R.id.refresh) {
this.webView.reload();
}
} else if (this.editing.booleanValue()) {
this.editButton.setText("Edit");
this.webView.loadUrl("https://477e4c09ef978f693ce0ea1dac93d31e.ctf.hacker101.com/content/");
this.editing = false;
} else {
this.editButton.setText("View");
this.webView.loadUrl("https://477e4c09ef978f693ce0ea1dac93d31e.ctf.hacker101.com/edit.php");
this.editing = true;
}
}
}

大致上是一個文件上傳/編輯網站,直接連進去 edit.php 找到一個 upload.php
image

進去之後發現只能上傳 zip,上傳後會顯示 Incorrect Hmac,接著會說大小為 16 bytes, 盲猜用 HMAC.MD5 簽

1
2
3
4
5
6
7
8
9
10
import hmac
import hashlib

file_content = open('l.zip', 'rb').read()
hmac_key = "8c34bac50d9b096d41cafb53683b315690acf65a11b5f63250c61f7718fa1d1d"
hmac_key_bytes = bytes.fromhex(hmac_key)

hmac_digest = hmac.new(hmac_key_bytes, file_content, hashlib.md5).hexdigest()
print(hmac_digest)

這是 Signer,最後送回去上傳就好
image
結果本來只是手癢開 hexedit 上 zip-slip 攻擊不小心就多一個 Flag 惹